mirror of
https://github.com/citra-emu/citra-nightly.git
synced 2025-09-26 18:27:02 +00:00
Compare commits
163 commits
nightly-20
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
0ff3440232 | ||
|
69e758d738 | ||
|
f4768cd26c | ||
|
e0d2c1308e | ||
|
4f9fc88bb3 | ||
|
d857743075 | ||
|
b5042a5257 | ||
|
e524542a40 | ||
|
3a4ebb1413 | ||
|
cbe8987036 | ||
|
da5aa70fc9 | ||
|
749a721aa2 | ||
|
bb003c2bd4 | ||
|
7638f87f74 | ||
|
aa6809e2a8 | ||
|
5e02be75a3 | ||
|
b9c9beeee5 | ||
|
de993dcfbd | ||
|
3c9157b1ec | ||
|
0c40c10022 | ||
|
2766118e33 | ||
|
06b26691ba | ||
|
d41ce64f7b | ||
|
1165a708d5 | ||
|
19784355f9 | ||
|
aa6a29d7e1 | ||
|
106364e01e | ||
|
d5a1bd07f3 | ||
|
8afa27718c | ||
|
8e2415f455 | ||
|
c978c074db | ||
|
cb92ec278e | ||
|
9f5d5c6ddd | ||
|
480604ec72 | ||
|
63feac6bb3 | ||
|
469f76b075 | ||
|
7a4854c519 | ||
|
d1e3dddf6a | ||
|
265e8193b9 | ||
|
e8c20fa782 | ||
|
95ae46f6a8 | ||
|
41fe75acb7 | ||
|
1744537d85 | ||
|
bea863efff | ||
|
89e13a85a7 | ||
|
549fdd0736 | ||
|
eddc4a029c | ||
|
82294425e3 | ||
|
77fce3cf82 | ||
|
8d82adb3d3 | ||
|
228f26d1e4 | ||
|
789654d7da | ||
|
ca3b2306d5 | ||
|
8e87bd606c | ||
|
f26044bb88 | ||
|
c59ef7d793 | ||
|
6a7841d4b0 | ||
|
a2d1c4a94c | ||
|
9c84721d84 | ||
|
cca8c08a9a | ||
|
72c1075402 | ||
|
30c53c9509 | ||
|
da9f382d2c | ||
|
a177769c3b | ||
|
f346949989 | ||
|
37f0a7484f | ||
|
19d5695aa3 | ||
|
6cbdc73f53 | ||
|
81ee7ad893 | ||
|
2ce0a9e899 | ||
|
015e42be05 | ||
|
57696b2c11 | ||
|
c8c2beaeff | ||
|
6069fac76d | ||
|
7bacb78ce3 | ||
|
0165012ba4 | ||
|
96aa1b3a08 | ||
|
b2c740ee0e | ||
|
bc352e8168 | ||
|
4f00eb20db | ||
|
8b6a9b0dd8 | ||
|
62409f8139 | ||
|
0df72f3873 | ||
|
f2ee9baec7 | ||
|
8e2037b3ff | ||
|
c6bcbc02de | ||
|
36db566428 | ||
|
9b147d3f9c | ||
|
7dd9174d31 | ||
|
5a7f615da1 | ||
|
811303ea54 | ||
|
5bcdcffd96 | ||
|
2bb7f89c30 | ||
|
602f4f60d8 | ||
|
3113ae6616 | ||
|
bd4ec251cd | ||
|
b6b98af105 | ||
|
60a280af24 | ||
|
178e602589 | ||
|
dccb8f6b17 | ||
|
f177433d41 | ||
|
71b88c4c1f | ||
|
c7e9f8449e | ||
|
2e369c03b8 | ||
|
a47d8a7b4d | ||
|
02ba5c652b | ||
|
762ddfd07b | ||
|
d680b79725 | ||
|
2b20082581 | ||
|
15ea0c6336 | ||
|
9a6d15ab74 | ||
|
60584e861d | ||
|
070853b465 | ||
|
24b5ffbfca | ||
|
4d9eedd0d8 | ||
|
59df319f48 | ||
|
875f5eaad5 | ||
|
ea9f522c0c | ||
|
55e0b02863 | ||
|
59beeac4c7 | ||
|
0ed909e782 | ||
|
9da78f6126 | ||
|
0842ee6d7b | ||
|
6ec079ede8 | ||
|
83b329f6e1 | ||
|
db7b929e47 | ||
|
dc8425a986 | ||
|
670e9936a4 | ||
|
c0ecdb689d | ||
|
68e6a2185d | ||
|
09b36c589b | ||
|
1dc0fa7bb5 | ||
|
85bd1be852 | ||
|
c17ec1d1aa | ||
|
33a1f27a99 | ||
|
5733c8681e | ||
|
f8ae41dfe3 | ||
|
52254537b7 | ||
|
98f17f8f04 | ||
|
ca6dae1744 | ||
|
b6acebcb11 | ||
|
ba702043f0 | ||
|
2a4c60c1dd | ||
|
a1532f813b | ||
|
26d5727b19 | ||
|
680e132318 | ||
|
90a5d989e7 | ||
|
de40153fa4 | ||
|
e9936e01c2 | ||
|
e28c2a390c | ||
|
63d1830429 | ||
|
88cc6acb4d | ||
|
3b31720c4d | ||
|
f9bbae81aa | ||
|
1c793deece | ||
|
d5b50a9fc0 | ||
|
168f168c33 | ||
|
312068eebf | ||
|
5118798c30 | ||
|
831c9c4a38 | ||
|
23ca10472a | ||
|
6f05dd9d1d | ||
|
19cc8e626b |
14
.ci/linux.sh
14
.ci/linux.sh
|
@ -1,13 +1,19 @@
|
||||||
#!/bin/sh -ex
|
#!/bin/bash -ex
|
||||||
|
|
||||||
|
if [ "$TARGET" = "appimage" ]; then
|
||||||
|
# Compile the AppImage we distribute with Clang.
|
||||||
|
export EXTRA_CMAKE_FLAGS=(-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DCMAKE_LINKER=/etc/bin/ld.lld)
|
||||||
|
else
|
||||||
|
# For the linux-fresh verification target, verify compilation without PCH as well.
|
||||||
|
export EXTRA_CMAKE_FLAGS=(-DCITRA_USE_PRECOMPILED_HEADERS=OFF)
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir build && cd build
|
mkdir build && cd build
|
||||||
cmake .. -G Ninja \
|
cmake .. -G Ninja \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||||
-DCMAKE_CXX_COMPILER=clang++ \
|
"${EXTRA_CMAKE_FLAGS[@]}" \
|
||||||
-DCMAKE_C_COMPILER=clang \
|
|
||||||
-DCMAKE_LINKER=/etc/bin/ld.lld \
|
|
||||||
-DENABLE_QT_TRANSLATION=ON \
|
-DENABLE_QT_TRANSLATION=ON \
|
||||||
-DCITRA_ENABLE_COMPATIBILITY_REPORTING=ON \
|
-DCITRA_ENABLE_COMPATIBILITY_REPORTING=ON \
|
||||||
-DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \
|
-DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \
|
||||||
|
|
16
.ci/pack.sh
16
.ci/pack.sh
|
@ -61,12 +61,20 @@ function pack_artifacts() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ -z "$PACK_INDIVIDUALLY" ]; then
|
if [ -n "$UNPACKED" ]; then
|
||||||
# Pack all of the artifacts at once.
|
# Copy the artifacts to be uploaded unpacked.
|
||||||
pack_artifacts build/bundle
|
for ARTIFACT in build/bundle/*; do
|
||||||
else
|
FILENAME=$(basename "$ARTIFACT")
|
||||||
|
EXTENSION="${FILENAME##*.}"
|
||||||
|
|
||||||
|
mv "$ARTIFACT" "artifacts/$REV_NAME.$EXTENSION"
|
||||||
|
done
|
||||||
|
elif [ -n "$PACK_INDIVIDUALLY" ]; then
|
||||||
# Pack and upload the artifacts one-by-one.
|
# Pack and upload the artifacts one-by-one.
|
||||||
for ARTIFACT in build/bundle/*; do
|
for ARTIFACT in build/bundle/*; do
|
||||||
pack_artifacts "$ARTIFACT"
|
pack_artifacts "$ARTIFACT"
|
||||||
done
|
done
|
||||||
|
else
|
||||||
|
# Pack all of the artifacts into a single archive.
|
||||||
|
pack_artifacts build/bundle
|
||||||
fi
|
fi
|
||||||
|
|
78
.github/workflows/build.yml
vendored
78
.github/workflows/build.yml
vendored
|
@ -12,13 +12,13 @@ jobs:
|
||||||
if: ${{ !github.head_ref }}
|
if: ${{ !github.head_ref }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Pack
|
- name: Pack
|
||||||
run: ./.ci/source.sh
|
run: ./.ci/source.sh
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: source
|
name: source
|
||||||
path: artifacts/
|
path: artifacts/
|
||||||
|
@ -33,15 +33,15 @@ jobs:
|
||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
CCACHE_SLOPPINESS: time_macros
|
||||||
OS: linux
|
OS: linux
|
||||||
TARGET: ${{ matrix.target }}
|
TARGET: ${{ matrix.target }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Set up cache
|
- name: Set up cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.CCACHE_DIR }}
|
path: ${{ env.CCACHE_DIR }}
|
||||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
||||||
|
@ -53,60 +53,60 @@ jobs:
|
||||||
run: ./.ci/pack.sh
|
run: ./.ci/pack.sh
|
||||||
if: ${{ matrix.target == 'appimage' }}
|
if: ${{ matrix.target == 'appimage' }}
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
if: ${{ matrix.target == 'appimage' }}
|
if: ${{ matrix.target == 'appimage' }}
|
||||||
with:
|
with:
|
||||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||||
path: artifacts/
|
path: artifacts/
|
||||||
macos:
|
macos:
|
||||||
runs-on: macos-13
|
runs-on: ${{ (matrix.target == 'x86_64' && 'macos-13') || 'macos-14' }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
target: ["x86_64", "arm64"]
|
target: ["x86_64", "arm64"]
|
||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
CCACHE_SLOPPINESS: time_macros
|
||||||
OS: macos
|
OS: macos
|
||||||
TARGET: ${{ matrix.target }}
|
TARGET: ${{ matrix.target }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Set up cache
|
- name: Set up cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.CCACHE_DIR }}
|
path: ${{ env.CCACHE_DIR }}
|
||||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ matrix.target }}-
|
${{ runner.os }}-${{ matrix.target }}-
|
||||||
- name: Install tools
|
- name: Install tools
|
||||||
run: brew install ccache glslang ninja
|
run: brew install ccache ninja
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./.ci/macos.sh
|
run: ./.ci/macos.sh
|
||||||
- name: Prepare outputs for caching
|
- name: Prepare outputs for caching
|
||||||
run: mv build/bundle $OS-$TARGET
|
run: mv build/bundle $OS-$TARGET
|
||||||
- name: Cache outputs for universal build
|
- name: Cache outputs for universal build
|
||||||
uses: actions/cache/save@v3
|
uses: actions/cache/save@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.OS }}-${{ env.TARGET }}
|
path: ${{ env.OS }}-${{ env.TARGET }}
|
||||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
macos-universal:
|
macos-universal:
|
||||||
runs-on: macos-13
|
runs-on: macos-14
|
||||||
needs: macos
|
needs: macos
|
||||||
env:
|
env:
|
||||||
OS: macos
|
OS: macos
|
||||||
TARGET: universal
|
TARGET: universal
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Download x86_64 build from cache
|
- name: Download x86_64 build from cache
|
||||||
uses: actions/cache/restore@v3
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.OS }}-x86_64
|
path: ${{ env.OS }}-x86_64
|
||||||
key: ${{ runner.os }}-x86_64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
key: ${{ runner.os }}-x86_64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
- name: Download ARM64 build from cache
|
- name: Download ARM64 build from cache
|
||||||
uses: actions/cache/restore@v3
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.OS }}-arm64
|
path: ${{ env.OS }}-arm64
|
||||||
key: ${{ runner.os }}-arm64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
key: ${{ runner.os }}-arm64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
|
@ -118,7 +118,7 @@ jobs:
|
||||||
- name: Pack
|
- name: Pack
|
||||||
run: ./.ci/pack.sh
|
run: ./.ci/pack.sh
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||||
path: artifacts/
|
path: artifacts/
|
||||||
|
@ -133,15 +133,15 @@ jobs:
|
||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
CCACHE_SLOPPINESS: time_macros
|
||||||
OS: windows
|
OS: windows
|
||||||
TARGET: ${{ matrix.target }}
|
TARGET: ${{ matrix.target }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Set up cache
|
- name: Set up cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.CCACHE_DIR }}
|
path: ${{ env.CCACHE_DIR }}
|
||||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
||||||
|
@ -153,13 +153,6 @@ jobs:
|
||||||
- name: Install extra tools (MSVC)
|
- name: Install extra tools (MSVC)
|
||||||
run: choco install ccache ninja wget
|
run: choco install ccache ninja wget
|
||||||
if: ${{ matrix.target == 'msvc' }}
|
if: ${{ matrix.target == 'msvc' }}
|
||||||
- name: Set up Vulkan SDK (MSVC)
|
|
||||||
uses: humbletim/setup-vulkan-sdk@v1.2.0
|
|
||||||
if: ${{ matrix.target == 'msvc' }}
|
|
||||||
with:
|
|
||||||
vulkan-query-version: latest
|
|
||||||
vulkan-components: SPIRV-Tools, Glslang
|
|
||||||
vulkan-use-cache: true
|
|
||||||
- name: Set up MSYS2
|
- name: Set up MSYS2
|
||||||
uses: msys2/setup-msys2@v2
|
uses: msys2/setup-msys2@v2
|
||||||
if: ${{ matrix.target == 'msys2' }}
|
if: ${{ matrix.target == 'msys2' }}
|
||||||
|
@ -168,10 +161,8 @@ jobs:
|
||||||
update: true
|
update: true
|
||||||
install: git make p7zip
|
install: git make p7zip
|
||||||
pacboy: >-
|
pacboy: >-
|
||||||
toolchain:p ccache:p cmake:p ninja:p glslang:p
|
toolchain:p ccache:p cmake:p ninja:p
|
||||||
qt6-base:p qt6-multimedia:p qt6-multimedia-wmf:p qt6-tools:p qt6-translations:p
|
qt6-base:p qt6-multimedia:p qt6-multimedia-wmf:p qt6-tools:p qt6-translations:p
|
||||||
- name: Test glslang
|
|
||||||
run: glslang --version || glslangValidator --version
|
|
||||||
- name: Disable line ending translation
|
- name: Disable line ending translation
|
||||||
run: git config --global core.autocrlf input
|
run: git config --global core.autocrlf input
|
||||||
- name: Build
|
- name: Build
|
||||||
|
@ -179,7 +170,7 @@ jobs:
|
||||||
- name: Pack
|
- name: Pack
|
||||||
run: ./.ci/pack.sh
|
run: ./.ci/pack.sh
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||||
path: artifacts/
|
path: artifacts/
|
||||||
|
@ -188,15 +179,15 @@ jobs:
|
||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
CCACHE_SLOPPINESS: time_macros
|
||||||
OS: android
|
OS: android
|
||||||
TARGET: universal
|
TARGET: universal
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Set up cache
|
- name: Set up cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
|
@ -215,7 +206,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
sudo add-apt-repository -y ppa:theofficialgman/gpu-tools
|
sudo add-apt-repository -y ppa:theofficialgman/gpu-tools
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install ccache glslang-dev glslang-tools apksigner -y
|
sudo apt-get install ccache apksigner -y
|
||||||
- name: Build
|
- name: Build
|
||||||
run: JAVA_HOME=$JAVA_HOME_17_X64 ./.ci/android.sh
|
run: JAVA_HOME=$JAVA_HOME_17_X64 ./.ci/android.sh
|
||||||
env:
|
env:
|
||||||
|
@ -226,35 +217,34 @@ jobs:
|
||||||
run: ../../../.ci/pack.sh
|
run: ../../../.ci/pack.sh
|
||||||
working-directory: src/android/app
|
working-directory: src/android/app
|
||||||
env:
|
env:
|
||||||
PACK_INDIVIDUALLY: 1
|
UNPACKED: 1
|
||||||
SKIP_7Z: 1
|
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||||
path: src/android/app/artifacts/
|
path: src/android/app/artifacts/
|
||||||
ios:
|
ios:
|
||||||
runs-on: macos-13
|
runs-on: macos-14
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
CCACHE_SLOPPINESS: time_macros
|
||||||
OS: ios
|
OS: ios
|
||||||
TARGET: arm64
|
TARGET: arm64
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Set up cache
|
- name: Set up cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.CCACHE_DIR }}
|
path: ${{ env.CCACHE_DIR }}
|
||||||
key: ${{ runner.os }}-ios-${{ github.sha }}
|
key: ${{ runner.os }}-ios-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-ios-
|
${{ runner.os }}-ios-
|
||||||
- name: Install tools
|
- name: Install tools
|
||||||
run: brew install ccache glslang ninja
|
run: brew install ccache ninja
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./.ci/ios.sh
|
run: ./.ci/ios.sh
|
||||||
release:
|
release:
|
||||||
|
@ -262,7 +252,7 @@ jobs:
|
||||||
needs: [windows, linux, macos-universal, android, source]
|
needs: [windows, linux, macos-universal, android, source]
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
env:
|
env:
|
||||||
|
|
2
.github/workflows/format.yml
vendored
2
.github/workflows/format.yml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
||||||
image: citraemu/build-environments:linux-fresh
|
image: citraemu/build-environments:linux-fresh
|
||||||
options: -u 1001
|
options: -u 1001
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|
16
.github/workflows/publish.yml
vendored
16
.github/workflows/publish.yml
vendored
|
@ -20,11 +20,11 @@ jobs:
|
||||||
if: ${{ github.event.inputs.nightly != 'false' && github.repository == 'citra-emu/citra' }}
|
if: ${{ github.event.inputs.nightly != 'false' && github.repository == 'citra-emu/citra' }}
|
||||||
steps:
|
steps:
|
||||||
# this checkout is required to make sure the GitHub Actions scripts are available
|
# this checkout is required to make sure the GitHub Actions scripts are available
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
name: Pre-checkout
|
name: Pre-checkout
|
||||||
with:
|
with:
|
||||||
submodules: false
|
submodules: false
|
||||||
- uses: actions/github-script@v6
|
- uses: actions/github-script@v7
|
||||||
id: check-changes
|
id: check-changes
|
||||||
name: 'Check for new changes'
|
name: 'Check for new changes'
|
||||||
env:
|
env:
|
||||||
|
@ -38,7 +38,7 @@ jobs:
|
||||||
return checkBaseChanges(github, context);
|
return checkBaseChanges(github, context);
|
||||||
- run: npm install execa@5
|
- run: npm install execa@5
|
||||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
name: Checkout
|
name: Checkout
|
||||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||||
with:
|
with:
|
||||||
|
@ -46,7 +46,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.ALT_GITHUB_TOKEN }}
|
token: ${{ secrets.ALT_GITHUB_TOKEN }}
|
||||||
- uses: actions/github-script@v6
|
- uses: actions/github-script@v7
|
||||||
name: 'Update and tag new commits'
|
name: 'Update and tag new commits'
|
||||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||||
env:
|
env:
|
||||||
|
@ -62,11 +62,11 @@ jobs:
|
||||||
if: ${{ github.event.inputs.canary != 'false' && github.repository == 'citra-emu/citra' }}
|
if: ${{ github.event.inputs.canary != 'false' && github.repository == 'citra-emu/citra' }}
|
||||||
steps:
|
steps:
|
||||||
# this checkout is required to make sure the GitHub Actions scripts are available
|
# this checkout is required to make sure the GitHub Actions scripts are available
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
name: Pre-checkout
|
name: Pre-checkout
|
||||||
with:
|
with:
|
||||||
submodules: false
|
submodules: false
|
||||||
- uses: actions/github-script@v6
|
- uses: actions/github-script@v7
|
||||||
id: check-changes
|
id: check-changes
|
||||||
name: 'Check for new changes'
|
name: 'Check for new changes'
|
||||||
env:
|
env:
|
||||||
|
@ -79,7 +79,7 @@ jobs:
|
||||||
return checkCanaryChanges(github, context);
|
return checkCanaryChanges(github, context);
|
||||||
- run: npm install execa@5
|
- run: npm install execa@5
|
||||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
name: Checkout
|
name: Checkout
|
||||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||||
with:
|
with:
|
||||||
|
@ -87,7 +87,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.ALT_GITHUB_TOKEN }}
|
token: ${{ secrets.ALT_GITHUB_TOKEN }}
|
||||||
- uses: actions/github-script@v6
|
- uses: actions/github-script@v7
|
||||||
name: 'Check and merge canary changes'
|
name: 'Check and merge canary changes'
|
||||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||||
env:
|
env:
|
||||||
|
|
2
.github/workflows/transifex.yml
vendored
2
.github/workflows/transifex.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
||||||
container: citraemu/build-environments:linux-fresh
|
container: citraemu/build-environments:linux-fresh
|
||||||
if: ${{ github.repository == 'citra-emu/citra' }}
|
if: ${{ github.repository == 'citra-emu/citra' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
|
@ -79,9 +79,11 @@ option(ENABLE_OPENAL "Enables the OpenAL audio backend" ON)
|
||||||
|
|
||||||
CMAKE_DEPENDENT_OPTION(ENABLE_LIBUSB "Enable libusb for GameCube Adapter support" ON "NOT IOS" OFF)
|
CMAKE_DEPENDENT_OPTION(ENABLE_LIBUSB "Enable libusb for GameCube Adapter support" ON "NOT IOS" OFF)
|
||||||
|
|
||||||
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
|
CMAKE_DEPENDENT_OPTION(ENABLE_SOFTWARE_RENDERER "Enables the software renderer" ON "NOT ANDROID" OFF)
|
||||||
|
CMAKE_DEPENDENT_OPTION(ENABLE_OPENGL "Enables the OpenGL renderer" ON "NOT APPLE" OFF)
|
||||||
|
option(ENABLE_VULKAN "Enables the Vulkan renderer" ON)
|
||||||
|
|
||||||
CMAKE_DEPENDENT_OPTION(CITRA_ENABLE_BUNDLE_TARGET "Enable the distribution bundling target." ON "NOT ANDROID AND NOT IOS" OFF)
|
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
|
||||||
|
|
||||||
# Compile options
|
# Compile options
|
||||||
CMAKE_DEPENDENT_OPTION(COMPILE_WITH_DWARF "Add DWARF debugging information" ${IS_DEBUG_BUILD} "MINGW" OFF)
|
CMAKE_DEPENDENT_OPTION(COMPILE_WITH_DWARF "Add DWARF debugging information" ${IS_DEBUG_BUILD} "MINGW" OFF)
|
||||||
|
@ -245,6 +247,26 @@ if (ENABLE_QT)
|
||||||
if (ENABLE_QT_TRANSLATION)
|
if (ENABLE_QT_TRANSLATION)
|
||||||
find_package(Qt6 REQUIRED COMPONENTS LinguistTools)
|
find_package(Qt6 REQUIRED COMPONENTS LinguistTools)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (NOT DEFINED QT_TARGET_PATH)
|
||||||
|
# Determine the location of the compile target's Qt.
|
||||||
|
get_target_property(qtcore_path Qt6::Core LOCATION_Release)
|
||||||
|
string(FIND "${qtcore_path}" "/bin/" qtcore_path_bin_pos REVERSE)
|
||||||
|
string(FIND "${qtcore_path}" "/lib/" qtcore_path_lib_pos REVERSE)
|
||||||
|
if (qtcore_path_bin_pos GREATER qtcore_path_lib_pos)
|
||||||
|
string(SUBSTRING "${qtcore_path}" 0 ${qtcore_path_bin_pos} QT_TARGET_PATH)
|
||||||
|
else()
|
||||||
|
string(SUBSTRING "${qtcore_path}" 0 ${qtcore_path_lib_pos} QT_TARGET_PATH)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT DEFINED QT_HOST_PATH)
|
||||||
|
# Use the same for host Qt if none is defined.
|
||||||
|
set(QT_HOST_PATH "${QT_TARGET_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message(STATUS "Using target Qt at ${QT_TARGET_PATH}")
|
||||||
|
message(STATUS "Using host Qt at ${QT_HOST_PATH}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Use system tsl::robin_map if available (otherwise we fallback to version bundled with dynarmic)
|
# Use system tsl::robin_map if available (otherwise we fallback to version bundled with dynarmic)
|
||||||
|
@ -254,20 +276,22 @@ find_package(tsl-robin-map QUIET)
|
||||||
# ======================================
|
# ======================================
|
||||||
|
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
|
if (NOT IOS)
|
||||||
|
# Umbrella framework for everything GUI-related
|
||||||
|
find_library(COCOA_LIBRARY Cocoa REQUIRED)
|
||||||
|
endif()
|
||||||
|
find_library(AVFOUNDATION_LIBRARY AVFoundation REQUIRED)
|
||||||
|
find_library(IOSURFACE_LIBRARY IOSurface REQUIRED)
|
||||||
|
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${AVFOUNDATION_LIBRARY} ${IOSURFACE_LIBRARY} ${MOLTENVK_LIBRARY})
|
||||||
|
|
||||||
|
if (ENABLE_VULKAN)
|
||||||
if (NOT USE_SYSTEM_MOLTENVK)
|
if (NOT USE_SYSTEM_MOLTENVK)
|
||||||
download_moltenvk()
|
download_moltenvk()
|
||||||
endif()
|
endif()
|
||||||
find_library(MOLTENVK_LIBRARY MoltenVK REQUIRED)
|
find_library(MOLTENVK_LIBRARY MoltenVK REQUIRED)
|
||||||
message(STATUS "Using MoltenVK at ${MOLTENVK_LIBRARY}.")
|
message(STATUS "Using MoltenVK at ${MOLTENVK_LIBRARY}.")
|
||||||
|
set(PLATFORM_LIBRARIES ${PLATFORM_LIBRARIES} ${MOLTENVK_LIBRARY})
|
||||||
if (NOT IOS)
|
|
||||||
# Umbrella framework for everything GUI-related
|
|
||||||
find_library(COCOA_LIBRARY Cocoa REQUIRED)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_library(AVFOUNDATION_LIBRARY AVFoundation REQUIRED)
|
|
||||||
find_library(IOSURFACE_LIBRARY IOSurface REQUIRED)
|
|
||||||
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${AVFOUNDATION_LIBRARY} ${IOSURFACE_LIBRARY} ${MOLTENVK_LIBRARY})
|
|
||||||
elseif (WIN32)
|
elseif (WIN32)
|
||||||
set(PLATFORM_LIBRARIES winmm ws2_32)
|
set(PLATFORM_LIBRARIES winmm ws2_32)
|
||||||
if (MINGW)
|
if (MINGW)
|
||||||
|
@ -418,7 +442,8 @@ else()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Create target for outputting distributable bundles.
|
# Create target for outputting distributable bundles.
|
||||||
if (CITRA_ENABLE_BUNDLE_TARGET)
|
# Not supported for mobile platforms as distributables are built differently.
|
||||||
|
if (NOT ANDROID AND NOT IOS)
|
||||||
include(BundleTarget)
|
include(BundleTarget)
|
||||||
if (ENABLE_SDL2_FRONTEND)
|
if (ENABLE_SDL2_FRONTEND)
|
||||||
bundle_target(citra)
|
bundle_target(citra)
|
||||||
|
|
|
@ -2,37 +2,104 @@
|
||||||
if (BUNDLE_TARGET_EXECUTE)
|
if (BUNDLE_TARGET_EXECUTE)
|
||||||
# --- Bundling method logic ---
|
# --- Bundling method logic ---
|
||||||
|
|
||||||
|
function(symlink_safe_copy from to)
|
||||||
|
if (WIN32)
|
||||||
|
# Use cmake copy for maximum compatibility.
|
||||||
|
execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${from}" "${to}"
|
||||||
|
RESULT_VARIABLE cp_result)
|
||||||
|
else()
|
||||||
|
# Use native copy to turn symlinks into normal files.
|
||||||
|
execute_process(COMMAND cp -L "${from}" "${to}"
|
||||||
|
RESULT_VARIABLE cp_result)
|
||||||
|
endif()
|
||||||
|
if (NOT cp_result EQUAL "0")
|
||||||
|
message(FATAL_ERROR "cp \"${from}\" \"${to}\" failed: ${cp_result}")
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
function(bundle_qt executable_path)
|
function(bundle_qt executable_path)
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
|
# Perform standalone bundling first to copy over all used libraries, as windeployqt does not do this.
|
||||||
|
bundle_standalone("${executable_path}" "${EXECUTABLE_PATH}" "${BUNDLE_LIBRARY_PATHS}")
|
||||||
|
|
||||||
get_filename_component(executable_parent_dir "${executable_path}" DIRECTORY)
|
get_filename_component(executable_parent_dir "${executable_path}" DIRECTORY)
|
||||||
find_program(windeployqt_executable windeployqt6)
|
|
||||||
|
|
||||||
# Create a qt.conf file pointing to the app directory.
|
# Create a qt.conf file pointing to the app directory.
|
||||||
# This ensures Qt can find its plugins.
|
# This ensures Qt can find its plugins.
|
||||||
file(WRITE "${executable_parent_dir}/qt.conf" "[Paths]\nprefix = .")
|
file(WRITE "${executable_parent_dir}/qt.conf" "[Paths]\nPrefix = .")
|
||||||
|
|
||||||
|
find_program(windeployqt_executable windeployqt6 PATHS "${QT_HOST_PATH}/bin")
|
||||||
|
find_program(qtpaths_executable qtpaths6 PATHS "${QT_HOST_PATH}/bin")
|
||||||
|
|
||||||
|
# TODO: Hack around windeployqt's poor cross-compilation support by
|
||||||
|
# TODO: making a local copy with a prefix pointing to the target Qt.
|
||||||
|
if (NOT "${QT_HOST_PATH}" STREQUAL "${QT_TARGET_PATH}")
|
||||||
|
set(windeployqt_dir "${BINARY_PATH}/windeployqt_copy")
|
||||||
|
file(MAKE_DIRECTORY "${windeployqt_dir}")
|
||||||
|
symlink_safe_copy("${windeployqt_executable}" "${windeployqt_dir}/windeployqt.exe")
|
||||||
|
symlink_safe_copy("${qtpaths_executable}" "${windeployqt_dir}/qtpaths.exe")
|
||||||
|
symlink_safe_copy("${QT_HOST_PATH}/bin/Qt6Core.dll" "${windeployqt_dir}")
|
||||||
|
|
||||||
|
if (EXISTS "${QT_TARGET_PATH}/share")
|
||||||
|
# Unix-style Qt; we need to wire up the paths manually.
|
||||||
|
file(WRITE "${windeployqt_dir}/qt.conf" "\
|
||||||
|
[Paths]\n
|
||||||
|
Prefix = ${QT_TARGET_PATH}\n \
|
||||||
|
ArchData = ${QT_TARGET_PATH}/share/qt6\n \
|
||||||
|
Binaries = ${QT_TARGET_PATH}/bin\n \
|
||||||
|
Data = ${QT_TARGET_PATH}/share/qt6\n \
|
||||||
|
Documentation = ${QT_TARGET_PATH}/share/qt6/doc\n \
|
||||||
|
Headers = ${QT_TARGET_PATH}/include/qt6\n \
|
||||||
|
Libraries = ${QT_TARGET_PATH}/lib\n \
|
||||||
|
LibraryExecutables = ${QT_TARGET_PATH}/share/qt6/bin\n \
|
||||||
|
Plugins = ${QT_TARGET_PATH}/share/qt6/plugins\n \
|
||||||
|
QmlImports = ${QT_TARGET_PATH}/share/qt6/qml\n \
|
||||||
|
Translations = ${QT_TARGET_PATH}/share/qt6/translations\n \
|
||||||
|
")
|
||||||
|
else()
|
||||||
|
# Windows-style Qt; the defaults should suffice.
|
||||||
|
file(WRITE "${windeployqt_dir}/qt.conf" "[Paths]\nPrefix = ${QT_TARGET_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(windeployqt_executable "${windeployqt_dir}/windeployqt.exe")
|
||||||
|
set(qtpaths_executable "${windeployqt_dir}/qtpaths.exe")
|
||||||
|
endif()
|
||||||
|
|
||||||
message(STATUS "Executing windeployqt for executable ${executable_path}")
|
message(STATUS "Executing windeployqt for executable ${executable_path}")
|
||||||
execute_process(COMMAND "${windeployqt_executable}" "${executable_path}"
|
execute_process(COMMAND "${windeployqt_executable}" "${executable_path}"
|
||||||
|
--qtpaths "${qtpaths_executable}"
|
||||||
--no-compiler-runtime --no-system-d3d-compiler --no-opengl-sw --no-translations
|
--no-compiler-runtime --no-system-d3d-compiler --no-opengl-sw --no-translations
|
||||||
--plugindir "${executable_parent_dir}/plugins")
|
--plugindir "${executable_parent_dir}/plugins"
|
||||||
|
RESULT_VARIABLE windeployqt_result)
|
||||||
|
if (NOT windeployqt_result EQUAL "0")
|
||||||
|
message(FATAL_ERROR "windeployqt failed: ${windeployqt_result}")
|
||||||
|
endif()
|
||||||
|
|
||||||
# Remove the FFmpeg multimedia plugin as we don't include FFmpeg.
|
# Remove the FFmpeg multimedia plugin as we don't include FFmpeg.
|
||||||
# We want to use the Windows media plugin instead, which is also included.
|
# We want to use the Windows media plugin instead, which is also included.
|
||||||
file(REMOVE "${executable_parent_dir}/plugins/multimedia/ffmpegmediaplugin.dll")
|
file(REMOVE "${executable_parent_dir}/plugins/multimedia/ffmpegmediaplugin.dll")
|
||||||
elseif (APPLE)
|
elseif (APPLE)
|
||||||
get_filename_component(executable_name "${executable_path}" NAME_WE)
|
get_filename_component(executable_name "${executable_path}" NAME_WE)
|
||||||
find_program(MACDEPLOYQT_EXECUTABLE macdeployqt6)
|
find_program(macdeployqt_executable macdeployqt6 PATHS "${QT_HOST_PATH}/bin")
|
||||||
|
|
||||||
message(STATUS "Executing macdeployqt for executable ${executable_path}")
|
message(STATUS "Executing macdeployqt at \"${macdeployqt_executable}\" for executable \"${executable_path}\"")
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND "${MACDEPLOYQT_EXECUTABLE}"
|
COMMAND "${macdeployqt_executable}"
|
||||||
"${executable_path}"
|
"${executable_path}"
|
||||||
"-executable=${executable_path}/Contents/MacOS/${executable_name}"
|
"-executable=${executable_path}/Contents/MacOS/${executable_name}"
|
||||||
-always-overwrite)
|
-always-overwrite
|
||||||
|
RESULT_VARIABLE macdeployqt_result)
|
||||||
|
if (NOT macdeployqt_result EQUAL "0")
|
||||||
|
message(FATAL_ERROR "macdeployqt failed: ${macdeployqt_result}")
|
||||||
|
endif()
|
||||||
|
|
||||||
# Bundling libraries can rewrite path information and break code signatures of system libraries.
|
# Bundling libraries can rewrite path information and break code signatures of system libraries.
|
||||||
# Perform an ad-hoc re-signing on the whole app bundle to fix this.
|
# Perform an ad-hoc re-signing on the whole app bundle to fix this.
|
||||||
execute_process(COMMAND codesign --deep -fs - "${executable_path}")
|
execute_process(COMMAND codesign --deep -fs - "${executable_path}"
|
||||||
|
RESULT_VARIABLE codesign_result)
|
||||||
|
if (NOT codesign_result EQUAL "0")
|
||||||
|
message(FATAL_ERROR "codesign failed: ${codesign_result}")
|
||||||
|
endif()
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR "Unsupported OS for Qt bundling.")
|
message(FATAL_ERROR "Unsupported OS for Qt bundling.")
|
||||||
endif()
|
endif()
|
||||||
|
@ -44,9 +111,9 @@ if (BUNDLE_TARGET_EXECUTE)
|
||||||
|
|
||||||
if (enable_qt)
|
if (enable_qt)
|
||||||
# Find qmake to make sure the plugin uses the right version of Qt.
|
# Find qmake to make sure the plugin uses the right version of Qt.
|
||||||
find_program(QMAKE_EXECUTABLE qmake6)
|
find_program(qmake_executable qmake6 PATHS "${QT_HOST_PATH}/bin")
|
||||||
|
|
||||||
set(extra_linuxdeploy_env "QMAKE=${QMAKE_EXECUTABLE}")
|
set(extra_linuxdeploy_env "QMAKE=${qmake_executable}")
|
||||||
set(extra_linuxdeploy_args --plugin qt)
|
set(extra_linuxdeploy_args --plugin qt)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
@ -59,7 +126,11 @@ if (BUNDLE_TARGET_EXECUTE)
|
||||||
--executable "${executable_path}"
|
--executable "${executable_path}"
|
||||||
--icon-file "${source_path}/dist/citra.svg"
|
--icon-file "${source_path}/dist/citra.svg"
|
||||||
--desktop-file "${source_path}/dist/${executable_name}.desktop"
|
--desktop-file "${source_path}/dist/${executable_name}.desktop"
|
||||||
--appdir "${appdir_path}")
|
--appdir "${appdir_path}"
|
||||||
|
RESULT_VARIABLE linuxdeploy_appdir_result)
|
||||||
|
if (NOT linuxdeploy_appdir_result EQUAL "0")
|
||||||
|
message(FATAL_ERROR "linuxdeploy failed to create AppDir: ${linuxdeploy_appdir_result}")
|
||||||
|
endif()
|
||||||
|
|
||||||
if (enable_qt)
|
if (enable_qt)
|
||||||
set(qt_hook_file "${appdir_path}/apprun-hooks/linuxdeploy-plugin-qt-hook.sh")
|
set(qt_hook_file "${appdir_path}/apprun-hooks/linuxdeploy-plugin-qt-hook.sh")
|
||||||
|
@ -82,7 +153,11 @@ if (BUNDLE_TARGET_EXECUTE)
|
||||||
"OUTPUT=${bundle_dir}/${executable_name}.AppImage"
|
"OUTPUT=${bundle_dir}/${executable_name}.AppImage"
|
||||||
"${linuxdeploy_executable}"
|
"${linuxdeploy_executable}"
|
||||||
--output appimage
|
--output appimage
|
||||||
--appdir "${appdir_path}")
|
--appdir "${appdir_path}"
|
||||||
|
RESULT_VARIABLE linuxdeploy_appimage_result)
|
||||||
|
if (NOT linuxdeploy_appimage_result EQUAL "0")
|
||||||
|
message(FATAL_ERROR "linuxdeploy failed to create AppImage: ${linuxdeploy_appimage_result}")
|
||||||
|
endif()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
function(bundle_standalone executable_path original_executable_path bundle_library_paths)
|
function(bundle_standalone executable_path original_executable_path bundle_library_paths)
|
||||||
|
@ -109,16 +184,23 @@ if (BUNDLE_TARGET_EXECUTE)
|
||||||
file(MAKE_DIRECTORY ${lib_dir})
|
file(MAKE_DIRECTORY ${lib_dir})
|
||||||
foreach (lib_file IN LISTS resolved_deps)
|
foreach (lib_file IN LISTS resolved_deps)
|
||||||
message(STATUS "Bundling library ${lib_file}")
|
message(STATUS "Bundling library ${lib_file}")
|
||||||
# Use native copy to turn symlinks into normal files.
|
symlink_safe_copy("${lib_file}" "${lib_dir}")
|
||||||
execute_process(COMMAND cp -L "${lib_file}" "${lib_dir}")
|
|
||||||
endforeach()
|
endforeach()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Add libs directory to executable rpath where applicable.
|
# Add libs directory to executable rpath where applicable.
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
execute_process(COMMAND install_name_tool -add_rpath "@loader_path/libs" "${executable_path}")
|
execute_process(COMMAND install_name_tool -add_rpath "@loader_path/libs" "${executable_path}"
|
||||||
|
RESULT_VARIABLE install_name_tool_result)
|
||||||
|
if (NOT install_name_tool_result EQUAL "0")
|
||||||
|
message(FATAL_ERROR "install_name_tool failed: ${install_name_tool_result}")
|
||||||
|
endif()
|
||||||
elseif (UNIX)
|
elseif (UNIX)
|
||||||
execute_process(COMMAND patchelf --set-rpath '$ORIGIN/../libs' "${executable_path}")
|
execute_process(COMMAND patchelf --set-rpath '$ORIGIN/../libs' "${executable_path}"
|
||||||
|
RESULT_VARIABLE patchelf_result)
|
||||||
|
if (NOT patchelf_result EQUAL "0")
|
||||||
|
message(FATAL_ERROR "patchelf failed: ${patchelf_result}")
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
@ -127,7 +209,7 @@ if (BUNDLE_TARGET_EXECUTE)
|
||||||
set(bundle_dir ${BINARY_PATH}/bundle)
|
set(bundle_dir ${BINARY_PATH}/bundle)
|
||||||
|
|
||||||
# On Linux, always bundle an AppImage.
|
# On Linux, always bundle an AppImage.
|
||||||
if (DEFINED LINUXDEPLOY)
|
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
|
||||||
if (IN_PLACE)
|
if (IN_PLACE)
|
||||||
message(FATAL_ERROR "Cannot bundle for Linux in-place.")
|
message(FATAL_ERROR "Cannot bundle for Linux in-place.")
|
||||||
endif()
|
endif()
|
||||||
|
@ -146,14 +228,12 @@ if (BUNDLE_TARGET_EXECUTE)
|
||||||
|
|
||||||
if (BUNDLE_QT)
|
if (BUNDLE_QT)
|
||||||
bundle_qt("${bundled_executable_path}")
|
bundle_qt("${bundled_executable_path}")
|
||||||
endif()
|
else()
|
||||||
|
|
||||||
if (WIN32 OR NOT BUNDLE_QT)
|
|
||||||
bundle_standalone("${bundled_executable_path}" "${EXECUTABLE_PATH}" "${BUNDLE_LIBRARY_PATHS}")
|
bundle_standalone("${bundled_executable_path}" "${EXECUTABLE_PATH}" "${BUNDLE_LIBRARY_PATHS}")
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
else()
|
elseif (BUNDLE_TARGET_DOWNLOAD_LINUXDEPLOY)
|
||||||
# --- Bundling target creation logic ---
|
# --- linuxdeploy download logic ---
|
||||||
|
|
||||||
# Downloads and extracts a linuxdeploy component.
|
# Downloads and extracts a linuxdeploy component.
|
||||||
function(download_linuxdeploy_component base_dir name executable_name)
|
function(download_linuxdeploy_component base_dir name executable_name)
|
||||||
|
@ -161,7 +241,7 @@ else()
|
||||||
if (NOT EXISTS "${executable_file}")
|
if (NOT EXISTS "${executable_file}")
|
||||||
message(STATUS "Downloading ${executable_name}")
|
message(STATUS "Downloading ${executable_name}")
|
||||||
file(DOWNLOAD
|
file(DOWNLOAD
|
||||||
"https://github.com/linuxdeploy/${name}/releases/download/continuous/${executable_name}"
|
"https://github.com/${name}/releases/download/continuous/${executable_name}"
|
||||||
"${executable_file}" SHOW_PROGRESS)
|
"${executable_file}" SHOW_PROGRESS)
|
||||||
file(CHMOD "${executable_file}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE)
|
file(CHMOD "${executable_file}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE)
|
||||||
|
|
||||||
|
@ -170,7 +250,11 @@ else()
|
||||||
message(STATUS "Extracting ${executable_name}")
|
message(STATUS "Extracting ${executable_name}")
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND "${executable_file}" --appimage-extract
|
COMMAND "${executable_file}" --appimage-extract
|
||||||
WORKING_DIRECTORY "${base_dir}")
|
WORKING_DIRECTORY "${base_dir}"
|
||||||
|
RESULT_VARIABLE extract_result)
|
||||||
|
if (NOT extract_result EQUAL "0")
|
||||||
|
message(FATAL_ERROR "AppImage extract failed: ${extract_result}")
|
||||||
|
endif()
|
||||||
else()
|
else()
|
||||||
message(STATUS "Copying ${executable_name}")
|
message(STATUS "Copying ${executable_name}")
|
||||||
file(COPY "${executable_file}" DESTINATION "${base_dir}/squashfs-root/usr/bin/")
|
file(COPY "${executable_file}" DESTINATION "${base_dir}/squashfs-root/usr/bin/")
|
||||||
|
@ -178,11 +262,15 @@ else()
|
||||||
endif()
|
endif()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
# Adds a target to the bundle target, packing in required libraries.
|
# Download plugins first so they don't overwrite linuxdeploy's AppRun file.
|
||||||
# If in_place is true, the bundling will be done in-place as part of the specified target.
|
download_linuxdeploy_component("${LINUXDEPLOY_PATH}" "linuxdeploy/linuxdeploy-plugin-qt" "linuxdeploy-plugin-qt-${LINUXDEPLOY_ARCH}.AppImage")
|
||||||
function(bundle_target_internal target_name in_place)
|
download_linuxdeploy_component("${LINUXDEPLOY_PATH}" "darealshinji/linuxdeploy-plugin-checkrt" "linuxdeploy-plugin-checkrt.sh")
|
||||||
# Create base bundle target if it does not exist.
|
download_linuxdeploy_component("${LINUXDEPLOY_PATH}" "linuxdeploy/linuxdeploy" "linuxdeploy-${LINUXDEPLOY_ARCH}.AppImage")
|
||||||
if (NOT in_place AND NOT TARGET bundle)
|
else()
|
||||||
|
# --- Bundling target creation logic ---
|
||||||
|
|
||||||
|
# Creates the base bundle target with common files and pre-bundle steps.
|
||||||
|
function(create_base_bundle_target)
|
||||||
message(STATUS "Creating base bundle target")
|
message(STATUS "Creating base bundle target")
|
||||||
|
|
||||||
add_custom_target(bundle)
|
add_custom_target(bundle)
|
||||||
|
@ -204,63 +292,72 @@ else()
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
TARGET bundle
|
TARGET bundle
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/dist/scripting" "${CMAKE_BINARY_DIR}/bundle/scripting")
|
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/dist/scripting" "${CMAKE_BINARY_DIR}/bundle/scripting")
|
||||||
|
|
||||||
|
# On Linux, add a command to prepare linuxdeploy and any required plugins before any bundling occurs.
|
||||||
|
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
|
||||||
|
add_custom_command(
|
||||||
|
TARGET bundle
|
||||||
|
COMMAND ${CMAKE_COMMAND}
|
||||||
|
"-DBUNDLE_TARGET_DOWNLOAD_LINUXDEPLOY=1"
|
||||||
|
"-DLINUXDEPLOY_PATH=${CMAKE_BINARY_DIR}/externals/linuxdeploy"
|
||||||
|
"-DLINUXDEPLOY_ARCH=${CMAKE_HOST_SYSTEM_PROCESSOR}"
|
||||||
|
-P "${CMAKE_SOURCE_DIR}/CMakeModules/BundleTarget.cmake"
|
||||||
|
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Adds a target to the bundle target, packing in required libraries.
|
||||||
|
# If in_place is true, the bundling will be done in-place as part of the specified target.
|
||||||
|
function(bundle_target_internal target_name in_place)
|
||||||
|
# Create base bundle target if it does not exist.
|
||||||
|
if (NOT in_place AND NOT TARGET bundle)
|
||||||
|
create_base_bundle_target()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(BUNDLE_EXECUTABLE_PATH "$<TARGET_FILE:${target_name}>")
|
set(bundle_executable_path "$<TARGET_FILE:${target_name}>")
|
||||||
if (target_name MATCHES ".*qt")
|
if (target_name MATCHES ".*qt")
|
||||||
set(BUNDLE_QT ON)
|
set(bundle_qt ON)
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
# For Qt targets on Apple, expect an app bundle.
|
# For Qt targets on Apple, expect an app bundle.
|
||||||
set(BUNDLE_EXECUTABLE_PATH "$<TARGET_BUNDLE_DIR:${target_name}>")
|
set(bundle_executable_path "$<TARGET_BUNDLE_DIR:${target_name}>")
|
||||||
endif()
|
endif()
|
||||||
else()
|
else()
|
||||||
set(BUNDLE_QT OFF)
|
set(bundle_qt OFF)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Build a list of library search paths from prefix paths.
|
# Build a list of library search paths from prefix paths.
|
||||||
foreach(prefix_path IN LISTS CMAKE_PREFIX_PATH CMAKE_SYSTEM_PREFIX_PATH)
|
foreach(prefix_path IN LISTS CMAKE_FIND_ROOT_PATH CMAKE_PREFIX_PATH CMAKE_SYSTEM_PREFIX_PATH)
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
list(APPEND BUNDLE_LIBRARY_PATHS "${prefix_path}/bin")
|
list(APPEND bundle_library_paths "${prefix_path}/bin")
|
||||||
endif()
|
endif()
|
||||||
list(APPEND BUNDLE_LIBRARY_PATHS "${prefix_path}/lib")
|
list(APPEND bundle_library_paths "${prefix_path}/lib")
|
||||||
endforeach()
|
endforeach()
|
||||||
foreach(library_path IN LISTS CMAKE_SYSTEM_LIBRARY_PATH)
|
foreach(library_path IN LISTS CMAKE_SYSTEM_LIBRARY_PATH)
|
||||||
list(APPEND BUNDLE_LIBRARY_PATHS "${library_path}")
|
list(APPEND bundle_library_paths "${library_path}")
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
# On Linux, prepare linuxdeploy and any required plugins.
|
|
||||||
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
|
||||||
set(LINUXDEPLOY_BASE "${CMAKE_BINARY_DIR}/externals/linuxdeploy")
|
|
||||||
|
|
||||||
# Download plugins first so they don't overwrite linuxdeploy's AppRun file.
|
|
||||||
download_linuxdeploy_component("${LINUXDEPLOY_BASE}" "linuxdeploy-plugin-qt" "linuxdeploy-plugin-qt-x86_64.AppImage")
|
|
||||||
download_linuxdeploy_component("${LINUXDEPLOY_BASE}" "linuxdeploy-plugin-checkrt" "linuxdeploy-plugin-checkrt-x86_64.sh")
|
|
||||||
download_linuxdeploy_component("${LINUXDEPLOY_BASE}" "linuxdeploy" "linuxdeploy-x86_64.AppImage")
|
|
||||||
|
|
||||||
set(EXTRA_BUNDLE_ARGS "-DLINUXDEPLOY=${LINUXDEPLOY_BASE}/squashfs-root/AppRun")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if (in_place)
|
if (in_place)
|
||||||
message(STATUS "Adding in-place bundling to ${target_name}")
|
message(STATUS "Adding in-place bundling to ${target_name}")
|
||||||
set(DEST_TARGET ${target_name})
|
set(dest_target ${target_name})
|
||||||
else()
|
else()
|
||||||
message(STATUS "Adding ${target_name} to bundle target")
|
message(STATUS "Adding ${target_name} to bundle target")
|
||||||
set(DEST_TARGET bundle)
|
set(dest_target bundle)
|
||||||
add_dependencies(bundle ${target_name})
|
add_dependencies(bundle ${target_name})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_custom_command(TARGET ${DEST_TARGET} POST_BUILD
|
add_custom_command(TARGET ${dest_target} POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND}
|
COMMAND ${CMAKE_COMMAND}
|
||||||
"-DCMAKE_PREFIX_PATH=\"${CMAKE_PREFIX_PATH}\""
|
"-DQT_HOST_PATH=\"${QT_HOST_PATH}\""
|
||||||
|
"-DQT_TARGET_PATH=\"${QT_TARGET_PATH}\""
|
||||||
"-DBUNDLE_TARGET_EXECUTE=1"
|
"-DBUNDLE_TARGET_EXECUTE=1"
|
||||||
"-DTARGET=${target_name}"
|
"-DTARGET=${target_name}"
|
||||||
"-DSOURCE_PATH=${CMAKE_SOURCE_DIR}"
|
"-DSOURCE_PATH=${CMAKE_SOURCE_DIR}"
|
||||||
"-DBINARY_PATH=${CMAKE_BINARY_DIR}"
|
"-DBINARY_PATH=${CMAKE_BINARY_DIR}"
|
||||||
"-DEXECUTABLE_PATH=${BUNDLE_EXECUTABLE_PATH}"
|
"-DEXECUTABLE_PATH=${bundle_executable_path}"
|
||||||
"-DBUNDLE_LIBRARY_PATHS=\"${BUNDLE_LIBRARY_PATHS}\""
|
"-DBUNDLE_LIBRARY_PATHS=\"${bundle_library_paths}\""
|
||||||
"-DBUNDLE_QT=${BUNDLE_QT}"
|
"-DBUNDLE_QT=${bundle_qt}"
|
||||||
"-DIN_PLACE=${in_place}"
|
"-DIN_PLACE=${in_place}"
|
||||||
${EXTRA_BUNDLE_ARGS}
|
"-DLINUXDEPLOY=${CMAKE_BINARY_DIR}/externals/linuxdeploy/squashfs-root/AppRun"
|
||||||
-P "${CMAKE_SOURCE_DIR}/CMakeModules/BundleTarget.cmake"
|
-P "${CMAKE_SOURCE_DIR}/CMakeModules/BundleTarget.cmake"
|
||||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}")
|
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
|
|
||||||
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
|
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||||
|
|
||||||
# This function downloads Qt using aqt. The path of the downloaded content will be added to the CMAKE_PREFIX_PATH.
|
# Determines parameters based on the host and target for downloading the right Qt binaries.
|
||||||
# Params:
|
function(determine_qt_parameters target host_out type_out arch_out arch_path_out host_type_out host_arch_out host_arch_path_out)
|
||||||
# target: Qt dependency to install. Specify a version number to download Qt, or "tools_(name)" for a specific build tool.
|
|
||||||
function(download_qt target)
|
|
||||||
if (target MATCHES "tools_.*")
|
if (target MATCHES "tools_.*")
|
||||||
set(DOWNLOAD_QT_TOOL ON)
|
set(tool ON)
|
||||||
else()
|
else()
|
||||||
set(DOWNLOAD_QT_TOOL OFF)
|
set(tool OFF)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Determine installation parameters for OS, architecture, and compiler
|
# Determine installation parameters for OS, architecture, and compiler
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
set(host "windows")
|
set(host "windows")
|
||||||
set(type "desktop")
|
set(type "desktop")
|
||||||
if (NOT DOWNLOAD_QT_TOOL)
|
|
||||||
|
if (NOT tool)
|
||||||
if (MINGW)
|
if (MINGW)
|
||||||
set(arch "win64_mingw")
|
set(arch "win64_mingw")
|
||||||
set(arch_path "mingw_64")
|
set(arch_path "mingw_64")
|
||||||
|
@ -28,21 +27,35 @@ function(download_qt target)
|
||||||
message(FATAL_ERROR "Unsupported bundled Qt architecture. Enable USE_SYSTEM_QT and provide your own.")
|
message(FATAL_ERROR "Unsupported bundled Qt architecture. Enable USE_SYSTEM_QT and provide your own.")
|
||||||
endif()
|
endif()
|
||||||
set(arch "win64_${arch_path}")
|
set(arch "win64_${arch_path}")
|
||||||
|
|
||||||
|
# In case we're cross-compiling, prepare to also fetch the correct host Qt tools.
|
||||||
|
if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "AMD64")
|
||||||
|
set(host_arch_path "msvc2019_64")
|
||||||
|
elseif (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "ARM64")
|
||||||
|
# TODO: msvc2019_arm64 doesn't include some of the required tools for some reason,
|
||||||
|
# TODO: so until it does, just use msvc2019_64 under x86_64 emulation.
|
||||||
|
# set(host_arch_path "msvc2019_arm64")
|
||||||
|
set(host_arch_path "msvc2019_64")
|
||||||
|
endif()
|
||||||
|
set(host_arch "win64_${host_arch_path}")
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR "Unsupported bundled Qt toolchain. Enable USE_SYSTEM_QT and provide your own.")
|
message(FATAL_ERROR "Unsupported bundled Qt toolchain. Enable USE_SYSTEM_QT and provide your own.")
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
elseif (APPLE)
|
elseif (APPLE)
|
||||||
set(host "mac")
|
set(host "mac")
|
||||||
if (IOS AND NOT DOWNLOAD_QT_TOOL)
|
|
||||||
set(type "ios")
|
|
||||||
set(arch "ios")
|
|
||||||
set(arch_path "ios")
|
|
||||||
set(host_arch_path "macos")
|
|
||||||
else()
|
|
||||||
set(type "desktop")
|
set(type "desktop")
|
||||||
set(arch "clang_64")
|
set(arch "clang_64")
|
||||||
set(arch_path "macos")
|
set(arch_path "macos")
|
||||||
|
|
||||||
|
if (IOS AND NOT tool)
|
||||||
|
set(host_type "${type}")
|
||||||
|
set(host_arch "${arch}")
|
||||||
|
set(host_arch_path "${arch_path}")
|
||||||
|
|
||||||
|
set(type "ios")
|
||||||
|
set(arch "ios")
|
||||||
|
set(arch_path "ios")
|
||||||
endif()
|
endif()
|
||||||
else()
|
else()
|
||||||
set(host "linux")
|
set(host "linux")
|
||||||
|
@ -51,38 +64,64 @@ function(download_qt target)
|
||||||
set(arch_path "linux")
|
set(arch_path "linux")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
get_external_prefix(qt base_path)
|
set(${host_out} "${host}" PARENT_SCOPE)
|
||||||
file(MAKE_DIRECTORY "${base_path}")
|
set(${type_out} "${type}" PARENT_SCOPE)
|
||||||
|
set(${arch_out} "${arch}" PARENT_SCOPE)
|
||||||
|
set(${arch_path_out} "${arch_path}" PARENT_SCOPE)
|
||||||
|
if (DEFINED host_type)
|
||||||
|
set(${host_type_out} "${host_type}" PARENT_SCOPE)
|
||||||
|
else()
|
||||||
|
set(${host_type_out} "${type}" PARENT_SCOPE)
|
||||||
|
endif()
|
||||||
|
if (DEFINED host_arch)
|
||||||
|
set(${host_arch_out} "${host_arch}" PARENT_SCOPE)
|
||||||
|
else()
|
||||||
|
set(${host_arch_out} "${arch}" PARENT_SCOPE)
|
||||||
|
endif()
|
||||||
|
if (DEFINED host_arch_path)
|
||||||
|
set(${host_arch_path_out} "${host_arch_path}" PARENT_SCOPE)
|
||||||
|
else()
|
||||||
|
set(${host_arch_path_out} "${arch_path}" PARENT_SCOPE)
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Download Qt binaries for a specifc configuration.
|
||||||
|
function(download_qt_configuration prefix_out target host type arch arch_path base_path)
|
||||||
|
if (target MATCHES "tools_.*")
|
||||||
|
set(tool ON)
|
||||||
|
else()
|
||||||
|
set(tool OFF)
|
||||||
|
endif()
|
||||||
|
|
||||||
set(install_args -c "${CURRENT_MODULE_DIR}/aqt_config.ini")
|
set(install_args -c "${CURRENT_MODULE_DIR}/aqt_config.ini")
|
||||||
if (DOWNLOAD_QT_TOOL)
|
if (tool)
|
||||||
set(prefix "${base_path}/Tools")
|
set(prefix "${base_path}/Tools")
|
||||||
set(install_args ${install_args} install-tool --outputdir ${base_path} ${host} desktop ${target})
|
set(install_args ${install_args} install-tool --outputdir ${base_path} ${host} desktop ${target})
|
||||||
else()
|
else()
|
||||||
set(prefix "${base_path}/${target}/${arch_path}")
|
set(prefix "${base_path}/${target}/${arch_path}")
|
||||||
if (host_arch_path)
|
set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch}
|
||||||
set(host_flag "--autodesktop")
|
|
||||||
set(host_prefix "${base_path}/${target}/${host_arch_path}")
|
|
||||||
endif()
|
|
||||||
set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} ${host_flag}
|
|
||||||
-m qtmultimedia --archives qttranslations qttools qtsvg qtbase)
|
-m qtmultimedia --archives qttranslations qttools qtsvg qtbase)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (NOT EXISTS "${prefix}")
|
if (NOT EXISTS "${prefix}")
|
||||||
message(STATUS "Downloading binaries for Qt...")
|
message(STATUS "Downloading Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path}")
|
||||||
set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.1.9")
|
set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.1.9")
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
set(aqt_path "${base_path}/aqt.exe")
|
set(aqt_path "${base_path}/aqt.exe")
|
||||||
|
if (NOT EXISTS "${aqt_path}")
|
||||||
file(DOWNLOAD
|
file(DOWNLOAD
|
||||||
${AQT_PREBUILD_BASE_URL}/aqt.exe
|
${AQT_PREBUILD_BASE_URL}/aqt.exe
|
||||||
${aqt_path} SHOW_PROGRESS)
|
${aqt_path} SHOW_PROGRESS)
|
||||||
|
endif()
|
||||||
execute_process(COMMAND ${aqt_path} ${install_args}
|
execute_process(COMMAND ${aqt_path} ${install_args}
|
||||||
WORKING_DIRECTORY ${base_path})
|
WORKING_DIRECTORY ${base_path})
|
||||||
elseif (APPLE)
|
elseif (APPLE)
|
||||||
set(aqt_path "${base_path}/aqt-macos")
|
set(aqt_path "${base_path}/aqt-macos")
|
||||||
|
if (NOT EXISTS "${aqt_path}")
|
||||||
file(DOWNLOAD
|
file(DOWNLOAD
|
||||||
${AQT_PREBUILD_BASE_URL}/aqt-macos
|
${AQT_PREBUILD_BASE_URL}/aqt-macos
|
||||||
${aqt_path} SHOW_PROGRESS)
|
${aqt_path} SHOW_PROGRESS)
|
||||||
|
endif()
|
||||||
execute_process(COMMAND chmod +x ${aqt_path})
|
execute_process(COMMAND chmod +x ${aqt_path})
|
||||||
execute_process(COMMAND ${aqt_path} ${install_args}
|
execute_process(COMMAND ${aqt_path} ${install_args}
|
||||||
WORKING_DIRECTORY ${base_path})
|
WORKING_DIRECTORY ${base_path})
|
||||||
|
@ -96,18 +135,38 @@ function(download_qt target)
|
||||||
execute_process(COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${aqt_install_path} python3 -m aqt ${install_args}
|
execute_process(COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${aqt_install_path} python3 -m aqt ${install_args}
|
||||||
WORKING_DIRECTORY ${base_path})
|
WORKING_DIRECTORY ${base_path})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
message(STATUS "Downloaded Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path} to ${prefix}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
message(STATUS "Using downloaded Qt binaries at ${prefix}")
|
set(${prefix_out} "${prefix}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
# Add the Qt prefix path so CMake can locate it.
|
# This function downloads Qt using aqt.
|
||||||
|
# The path of the downloaded content will be added to the CMAKE_PREFIX_PATH.
|
||||||
|
# QT_TARGET_PATH is set to the Qt for the compile target platform.
|
||||||
|
# QT_HOST_PATH is set to a host-compatible Qt, for running tools.
|
||||||
|
# Params:
|
||||||
|
# target: Qt dependency to install. Specify a version number to download Qt, or "tools_(name)" for a specific build tool.
|
||||||
|
function(download_qt target)
|
||||||
|
determine_qt_parameters("${target}" host type arch arch_path host_type host_arch host_arch_path)
|
||||||
|
|
||||||
|
get_external_prefix(qt base_path)
|
||||||
|
file(MAKE_DIRECTORY "${base_path}")
|
||||||
|
|
||||||
|
download_qt_configuration(prefix "${target}" "${host}" "${type}" "${arch}" "${arch_path}" "${base_path}")
|
||||||
|
if (DEFINED host_arch_path AND NOT "${host_arch_path}" STREQUAL "${arch_path}")
|
||||||
|
download_qt_configuration(host_prefix "${target}" "${host}" "${host_type}" "${host_arch}" "${host_arch_path}" "${base_path}")
|
||||||
|
else()
|
||||||
|
set(host_prefix "${prefix}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(QT_TARGET_PATH "${prefix}" CACHE STRING "")
|
||||||
|
set(QT_HOST_PATH "${host_prefix}" CACHE STRING "")
|
||||||
|
|
||||||
|
# Add the target Qt prefix path so CMake can locate it.
|
||||||
list(APPEND CMAKE_PREFIX_PATH "${prefix}")
|
list(APPEND CMAKE_PREFIX_PATH "${prefix}")
|
||||||
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE)
|
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE)
|
||||||
|
|
||||||
if (DEFINED host_prefix)
|
|
||||||
message(STATUS "Using downloaded host Qt binaries at ${host_prefix}")
|
|
||||||
set(QT_HOST_PATH "${host_prefix}" CACHE STRING "")
|
|
||||||
endif()
|
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
function(download_moltenvk)
|
function(download_moltenvk)
|
||||||
|
@ -121,7 +180,7 @@ function(download_moltenvk)
|
||||||
set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar")
|
set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar")
|
||||||
if (NOT EXISTS ${MOLTENVK_DIR})
|
if (NOT EXISTS ${MOLTENVK_DIR})
|
||||||
if (NOT EXISTS ${MOLTENVK_TAR})
|
if (NOT EXISTS ${MOLTENVK_TAR})
|
||||||
file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/latest/download/MoltenVK-all.tar
|
file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/v1.2.7-rc2/MoltenVK-all.tar
|
||||||
${MOLTENVK_TAR} SHOW_PROGRESS)
|
${MOLTENVK_TAR} SHOW_PROGRESS)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
|
@ -26,16 +26,14 @@ set(HASH_FILES
|
||||||
"${VIDEO_CORE}/shader/generator/spv_fs_shader_gen.h"
|
"${VIDEO_CORE}/shader/generator/spv_fs_shader_gen.h"
|
||||||
"${VIDEO_CORE}/shader/shader.cpp"
|
"${VIDEO_CORE}/shader/shader.cpp"
|
||||||
"${VIDEO_CORE}/shader/shader.h"
|
"${VIDEO_CORE}/shader/shader.h"
|
||||||
"${VIDEO_CORE}/pica.cpp"
|
"${VIDEO_CORE}/pica/regs_framebuffer.h"
|
||||||
"${VIDEO_CORE}/pica.h"
|
"${VIDEO_CORE}/pica/regs_lighting.h"
|
||||||
"${VIDEO_CORE}/regs_framebuffer.h"
|
"${VIDEO_CORE}/pica/regs_pipeline.h"
|
||||||
"${VIDEO_CORE}/regs_lighting.h"
|
"${VIDEO_CORE}/pica/regs_rasterizer.h"
|
||||||
"${VIDEO_CORE}/regs_pipeline.h"
|
"${VIDEO_CORE}/pica/regs_shader.h"
|
||||||
"${VIDEO_CORE}/regs_rasterizer.h"
|
"${VIDEO_CORE}/pica/regs_texturing.h"
|
||||||
"${VIDEO_CORE}/regs_shader.h"
|
"${VIDEO_CORE}/pica/regs_internal.cpp"
|
||||||
"${VIDEO_CORE}/regs_texturing.h"
|
"${VIDEO_CORE}/pica/regs_internal.h"
|
||||||
"${VIDEO_CORE}/regs.cpp"
|
|
||||||
"${VIDEO_CORE}/regs.h"
|
|
||||||
)
|
)
|
||||||
set(COMBINED "")
|
set(COMBINED "")
|
||||||
foreach (F IN LISTS HASH_FILES)
|
foreach (F IN LISTS HASH_FILES)
|
||||||
|
|
34
dist/apple/Info.plist.in
vendored
34
dist/apple/Info.plist.in
vendored
|
@ -21,9 +21,43 @@
|
||||||
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
|
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
|
||||||
<!-- Fixed -->
|
<!-- Fixed -->
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.games</string>
|
<string>public.app-category.games</string>
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>3ds</string>
|
||||||
|
<string>3dsx</string>
|
||||||
|
<string>cci</string>
|
||||||
|
<string>cxi</string>
|
||||||
|
<string>cia</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Nintendo 3DS File</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Default</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>elf</string>
|
||||||
|
<string>axf</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Unix Executable and Linkable Format</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>This app requires camera access to emulate the 3DS's cameras.</string>
|
<string>This app requires camera access to emulate the 3DS's cameras.</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
|
8
dist/dumpkeys/DumpKeys.gm9
vendored
8
dist/dumpkeys/DumpKeys.gm9
vendored
|
@ -287,5 +287,13 @@ dumptxt -p $[OUT] "nfcSecret1Seed=$[NFC_SEED_1]"
|
||||||
dumptxt -p $[OUT] "nfcSecret1HmacKey=$[NFC_HMAC_KEY_1]"
|
dumptxt -p $[OUT] "nfcSecret1HmacKey=$[NFC_HMAC_KEY_1]"
|
||||||
dumptxt -p $[OUT] "nfcIv=$[NFC_IV]"
|
dumptxt -p $[OUT] "nfcIv=$[NFC_IV]"
|
||||||
|
|
||||||
|
# Dump seeddb.bin as well
|
||||||
|
|
||||||
|
set SEEDDB_IN "0:/gm9/out/seeddb.bin"
|
||||||
|
set SEEDDB_OUT "0:/gm9/seeddb.bin"
|
||||||
|
|
||||||
|
sdump -w seeddb.bin
|
||||||
|
cp -w $[SEEDDB_IN] $[SEEDDB_OUT]
|
||||||
|
|
||||||
@Exit
|
@Exit
|
||||||
|
|
||||||
|
|
2
dist/dumpkeys/README.md
vendored
2
dist/dumpkeys/README.md
vendored
|
@ -6,5 +6,5 @@ Usage:
|
||||||
1. Copy "DumpKeys.gm9" into the "gm9/scripts/" directory on your SD card.
|
1. Copy "DumpKeys.gm9" into the "gm9/scripts/" directory on your SD card.
|
||||||
2. Launch GodMode9, press the HOME button, select Scripts, and select "DumpKeys" from the list of scripts that appears.
|
2. Launch GodMode9, press the HOME button, select Scripts, and select "DumpKeys" from the list of scripts that appears.
|
||||||
3. Wait for the script to complete and return you to the GodMode9 main menu.
|
3. Wait for the script to complete and return you to the GodMode9 main menu.
|
||||||
4. Power off your system and copy the "gm9/aes_keys.txt" file off of your SD card into "(Citra directory)/sysdata/".
|
4. Power off your system and copy the "gm9/aes_keys.txt" and "gm9/seeddb.bin" files off of your SD card into "(Citra directory)/sysdata/".
|
||||||
|
|
||||||
|
|
5
dist/languages/.tx/config
vendored
5
dist/languages/.tx/config
vendored
|
@ -7,3 +7,8 @@ source_file = en.ts
|
||||||
source_lang = en
|
source_lang = en
|
||||||
type = QT
|
type = QT
|
||||||
|
|
||||||
|
[o:citra:p:citra:r:android]
|
||||||
|
file_filter = ../../src/android/app/src/main/res/values-<lang>/strings.xml
|
||||||
|
source_file = ../../src/android/app/src/main/res/values/strings.xml
|
||||||
|
type = ANDROID
|
||||||
|
lang_map = es_ES:es, hu_HU:hu, ru_RU:ru, pt_BR:pt, zh_CN:zh
|
||||||
|
|
1111
dist/languages/da_DK.ts
vendored
1111
dist/languages/da_DK.ts
vendored
File diff suppressed because it is too large
Load diff
1670
dist/languages/de.ts
vendored
1670
dist/languages/de.ts
vendored
File diff suppressed because it is too large
Load diff
1105
dist/languages/el.ts
vendored
1105
dist/languages/el.ts
vendored
File diff suppressed because it is too large
Load diff
1152
dist/languages/es_ES.ts
vendored
1152
dist/languages/es_ES.ts
vendored
File diff suppressed because it is too large
Load diff
1111
dist/languages/fi.ts
vendored
1111
dist/languages/fi.ts
vendored
File diff suppressed because it is too large
Load diff
1151
dist/languages/fr.ts
vendored
1151
dist/languages/fr.ts
vendored
File diff suppressed because it is too large
Load diff
5229
dist/languages/fi_FI.ts → dist/languages/hu_HU.ts
vendored
5229
dist/languages/fi_FI.ts → dist/languages/hu_HU.ts
vendored
File diff suppressed because it is too large
Load diff
1111
dist/languages/id.ts
vendored
1111
dist/languages/id.ts
vendored
File diff suppressed because it is too large
Load diff
1331
dist/languages/it.ts
vendored
1331
dist/languages/it.ts
vendored
File diff suppressed because it is too large
Load diff
1105
dist/languages/ja_JP.ts
vendored
1105
dist/languages/ja_JP.ts
vendored
File diff suppressed because it is too large
Load diff
1107
dist/languages/ko_KR.ts
vendored
1107
dist/languages/ko_KR.ts
vendored
File diff suppressed because it is too large
Load diff
1111
dist/languages/lt_LT.ts
vendored
1111
dist/languages/lt_LT.ts
vendored
File diff suppressed because it is too large
Load diff
1105
dist/languages/nb.ts
vendored
1105
dist/languages/nb.ts
vendored
File diff suppressed because it is too large
Load diff
1107
dist/languages/nl.ts
vendored
1107
dist/languages/nl.ts
vendored
File diff suppressed because it is too large
Load diff
1111
dist/languages/pl_PL.ts
vendored
1111
dist/languages/pl_PL.ts
vendored
File diff suppressed because it is too large
Load diff
1180
dist/languages/pt_BR.ts
vendored
1180
dist/languages/pt_BR.ts
vendored
File diff suppressed because it is too large
Load diff
1182
dist/languages/ro_RO.ts
vendored
1182
dist/languages/ro_RO.ts
vendored
File diff suppressed because it is too large
Load diff
1105
dist/languages/ru_RU.ts
vendored
1105
dist/languages/ru_RU.ts
vendored
File diff suppressed because it is too large
Load diff
1105
dist/languages/tr_TR.ts
vendored
1105
dist/languages/tr_TR.ts
vendored
File diff suppressed because it is too large
Load diff
1117
dist/languages/vi_VN.ts
vendored
1117
dist/languages/vi_VN.ts
vendored
File diff suppressed because it is too large
Load diff
1113
dist/languages/zh_CN.ts
vendored
1113
dist/languages/zh_CN.ts
vendored
File diff suppressed because it is too large
Load diff
1111
dist/languages/zh_TW.ts
vendored
1111
dist/languages/zh_TW.ts
vendored
File diff suppressed because it is too large
Load diff
13
dist/qt_themes/default/style.qss
vendored
13
dist/qt_themes/default/style.qss
vendored
|
@ -12,18 +12,19 @@ QPushButton#GraphicsAPIStatusBarButton:hover {
|
||||||
border: 1px solid #76797C;
|
border: 1px solid #76797C;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton#3DOptionStatusBarButton {
|
QPushButton#TogglableStatusBarButton {
|
||||||
color: #A5A5A5;
|
color: #959595;
|
||||||
font-weight: bold;
|
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
padding: 0px 3px 0px 3px;
|
padding: 0px 3px 0px 3px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
min-width: 60px;
|
|
||||||
min-height: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton#3DOptionStatusBarButton:hover {
|
QPushButton#TogglableStatusBarButton:checked {
|
||||||
|
color: #00FF00;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton#TogglableStatusBarButton:hover {
|
||||||
border: 1px solid #76797C;
|
border: 1px solid #76797C;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
33
dist/qt_themes/qdarkstyle/style.qss
vendored
33
dist/qt_themes/qdarkstyle/style.qss
vendored
|
@ -1,19 +1,3 @@
|
||||||
QPushButton#TogglableStatusBarButton {
|
|
||||||
color: #959595;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0px 3px 0px 3px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#TogglableStatusBarButton:checked {
|
|
||||||
color: palette(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#TogglableStatusBarButton:hover {
|
|
||||||
border: 1px solid #76797C;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#GraphicsAPIStatusBarButton {
|
QPushButton#GraphicsAPIStatusBarButton {
|
||||||
color: #656565;
|
color: #656565;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
@ -26,6 +10,23 @@ QPushButton#GraphicsAPIStatusBarButton:hover {
|
||||||
border: 1px solid #76797C;
|
border: 1px solid #76797C;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QPushButton#TogglableStatusBarButton {
|
||||||
|
min-width: 0px;
|
||||||
|
color: #656565;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0px 3px 0px 3px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton#TogglableStatusBarButton:checked {
|
||||||
|
color: #00FF00;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton#TogglableStatusBarButton:hover {
|
||||||
|
border: 1px solid #76797C;
|
||||||
|
}
|
||||||
|
|
||||||
QToolTip {
|
QToolTip {
|
||||||
border: 1px solid #76797C;
|
border: 1px solid #76797C;
|
||||||
background-color: #5A7566;
|
background-color: #5A7566;
|
||||||
|
|
105
externals/CMakeLists.txt
vendored
105
externals/CMakeLists.txt
vendored
|
@ -41,9 +41,15 @@ else()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Catch2
|
# Catch2
|
||||||
|
add_library(catch2 INTERFACE)
|
||||||
|
if(USE_SYSTEM_CATCH2)
|
||||||
|
find_package(Catch2 3.0.0 REQUIRED)
|
||||||
|
else()
|
||||||
set(CATCH_INSTALL_DOCS OFF CACHE BOOL "")
|
set(CATCH_INSTALL_DOCS OFF CACHE BOOL "")
|
||||||
set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "")
|
set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "")
|
||||||
add_subdirectory(catch2)
|
add_subdirectory(catch2)
|
||||||
|
endif()
|
||||||
|
target_link_libraries(catch2 INTERFACE Catch2::Catch2WithMain)
|
||||||
|
|
||||||
# Crypto++
|
# Crypto++
|
||||||
if(USE_SYSTEM_CRYPTOPP)
|
if(USE_SYSTEM_CRYPTOPP)
|
||||||
|
@ -51,6 +57,12 @@ if(USE_SYSTEM_CRYPTOPP)
|
||||||
add_library(cryptopp INTERFACE)
|
add_library(cryptopp INTERFACE)
|
||||||
target_link_libraries(cryptopp INTERFACE cryptopp::cryptopp)
|
target_link_libraries(cryptopp INTERFACE cryptopp::cryptopp)
|
||||||
else()
|
else()
|
||||||
|
if (WIN32 AND NOT MSVC AND "arm64" IN_LIST ARCHITECTURE)
|
||||||
|
# TODO: CryptoPP ARM64 ASM does not seem to support Windows unless compiled with MSVC.
|
||||||
|
# TODO: See https://github.com/weidai11/cryptopp/issues/1260
|
||||||
|
set(CRYPTOPP_DISABLE_ASM ON CACHE BOOL "")
|
||||||
|
endif()
|
||||||
|
|
||||||
set(CRYPTOPP_BUILD_DOCUMENTATION OFF CACHE BOOL "")
|
set(CRYPTOPP_BUILD_DOCUMENTATION OFF CACHE BOOL "")
|
||||||
set(CRYPTOPP_BUILD_TESTING OFF CACHE BOOL "")
|
set(CRYPTOPP_BUILD_TESTING OFF CACHE BOOL "")
|
||||||
set(CRYPTOPP_INSTALL OFF CACHE BOOL "")
|
set(CRYPTOPP_INSTALL OFF CACHE BOOL "")
|
||||||
|
@ -114,29 +126,6 @@ if (MSVC)
|
||||||
add_subdirectory(getopt)
|
add_subdirectory(getopt)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Glad
|
|
||||||
add_subdirectory(glad)
|
|
||||||
|
|
||||||
# glslang
|
|
||||||
if(USE_SYSTEM_GLSLANG)
|
|
||||||
find_package(glslang REQUIRED)
|
|
||||||
add_library(glslang INTERFACE)
|
|
||||||
add_library(SPIRV INTERFACE)
|
|
||||||
target_link_libraries(glslang INTERFACE glslang::glslang)
|
|
||||||
target_link_libraries(SPIRV INTERFACE glslang::SPIRV)
|
|
||||||
# System include path is different from submodule include path
|
|
||||||
get_target_property(GLSLANG_PREFIX glslang::SPIRV INTERFACE_INCLUDE_DIRECTORIES)
|
|
||||||
target_include_directories(SPIRV SYSTEM INTERFACE "${GLSLANG_PREFIX}/glslang")
|
|
||||||
else()
|
|
||||||
set(SKIP_GLSLANG_INSTALL ON CACHE BOOL "")
|
|
||||||
set(ENABLE_GLSLANG_BINARIES OFF CACHE BOOL "")
|
|
||||||
set(ENABLE_SPVREMAPPER OFF CACHE BOOL "")
|
|
||||||
set(ENABLE_CTEST OFF CACHE BOOL "")
|
|
||||||
set(ENABLE_HLSL OFF CACHE BOOL "")
|
|
||||||
set(BUILD_EXTERNAL OFF CACHE BOOL "")
|
|
||||||
add_subdirectory(glslang)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# inih
|
# inih
|
||||||
if(USE_SYSTEM_INIH)
|
if(USE_SYSTEM_INIH)
|
||||||
find_package(inih REQUIRED COMPONENTS inih inir)
|
find_package(inih REQUIRED COMPONENTS inih inir)
|
||||||
|
@ -191,9 +180,6 @@ if(NOT USE_SYSTEM_SOUNDTOUCH)
|
||||||
target_compile_definitions(SoundTouch PUBLIC SOUNDTOUCH_INTEGER_SAMPLES)
|
target_compile_definitions(SoundTouch PUBLIC SOUNDTOUCH_INTEGER_SAMPLES)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# sirit
|
|
||||||
add_subdirectory(sirit EXCLUDE_FROM_ALL)
|
|
||||||
|
|
||||||
# Teakra
|
# Teakra
|
||||||
add_subdirectory(teakra EXCLUDE_FROM_ALL)
|
add_subdirectory(teakra EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
|
@ -255,6 +241,18 @@ endif()
|
||||||
|
|
||||||
# DiscordRPC
|
# DiscordRPC
|
||||||
if (USE_DISCORD_PRESENCE)
|
if (USE_DISCORD_PRESENCE)
|
||||||
|
# rapidjson used by discord-rpc is old and doesn't correctly detect endianness for some platforms.
|
||||||
|
include(TestBigEndian)
|
||||||
|
test_big_endian(RAPIDJSON_BIG_ENDIAN)
|
||||||
|
if(RAPIDJSON_BIG_ENDIAN)
|
||||||
|
add_compile_definitions(RAPIDJSON_ENDIAN=1)
|
||||||
|
else()
|
||||||
|
add_compile_definitions(RAPIDJSON_ENDIAN=0)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Apply a dummy CLANG_FORMAT_SUFFIX to disable discord-rpc's unnecessary automatic clang-format.
|
||||||
|
set(CLANG_FORMAT_SUFFIX "dummy")
|
||||||
|
|
||||||
add_subdirectory(discord-rpc EXCLUDE_FROM_ALL)
|
add_subdirectory(discord-rpc EXCLUDE_FROM_ALL)
|
||||||
target_include_directories(discord-rpc INTERFACE ./discord-rpc/include)
|
target_include_directories(discord-rpc INTERFACE ./discord-rpc/include)
|
||||||
endif()
|
endif()
|
||||||
|
@ -296,21 +294,29 @@ endif()
|
||||||
add_library(httplib INTERFACE)
|
add_library(httplib INTERFACE)
|
||||||
if(USE_SYSTEM_CPP_HTTPLIB)
|
if(USE_SYSTEM_CPP_HTTPLIB)
|
||||||
find_package(CppHttp 0.14.1)
|
find_package(CppHttp 0.14.1)
|
||||||
|
# Detect if system cpphttplib is a shared library
|
||||||
|
# this breaks building as Citra relies on functions that are moved
|
||||||
|
# into the shared object.
|
||||||
|
get_target_property(HTTP_LIBS httplib::httplib INTERFACE_LINK_LIBRARIES)
|
||||||
|
if(HTTP_LIBS)
|
||||||
|
message(WARNING "Shared cpp-http (${HTTP_LIBS}) not supported. Falling back to bundled...")
|
||||||
|
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
|
||||||
|
else()
|
||||||
if(CppHttp_FOUND)
|
if(CppHttp_FOUND)
|
||||||
target_link_libraries(httplib INTERFACE httplib::httplib)
|
target_link_libraries(httplib INTERFACE httplib::httplib)
|
||||||
else()
|
else()
|
||||||
message(STATUS "Cpp-httplib not found or not suitable version! Falling back to bundled...")
|
message(STATUS "Cpp-httplib not found or not suitable version! Falling back to bundled...")
|
||||||
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
|
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
|
||||||
endif()
|
endif()
|
||||||
|
endif()
|
||||||
else()
|
else()
|
||||||
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
|
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
|
||||||
endif()
|
endif()
|
||||||
target_compile_options(httplib INTERFACE -DCPPHTTPLIB_OPENSSL_SUPPORT)
|
target_compile_options(httplib INTERFACE -DCPPHTTPLIB_OPENSSL_SUPPORT)
|
||||||
target_link_libraries(httplib INTERFACE ${OPENSSL_LIBRARIES})
|
target_link_libraries(httplib INTERFACE ${OPENSSL_LIBRARIES})
|
||||||
|
|
||||||
if(ANDROID)
|
if (UNIX AND NOT APPLE)
|
||||||
add_subdirectory(android-ifaddrs)
|
add_subdirectory(gamemode)
|
||||||
target_link_libraries(httplib INTERFACE ifaddrs)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# cpp-jwt
|
# cpp-jwt
|
||||||
|
@ -361,6 +367,37 @@ if (ENABLE_OPENAL)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# OpenGL dependencies
|
||||||
|
if (ENABLE_OPENGL)
|
||||||
|
# Glad
|
||||||
|
add_subdirectory(glad)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Vulkan dependencies
|
||||||
|
if (ENABLE_VULKAN)
|
||||||
|
# glslang
|
||||||
|
if(USE_SYSTEM_GLSLANG)
|
||||||
|
find_package(glslang REQUIRED)
|
||||||
|
add_library(glslang INTERFACE)
|
||||||
|
add_library(SPIRV INTERFACE)
|
||||||
|
target_link_libraries(glslang INTERFACE glslang::glslang)
|
||||||
|
target_link_libraries(SPIRV INTERFACE glslang::SPIRV)
|
||||||
|
# System include path is different from submodule include path
|
||||||
|
get_target_property(GLSLANG_PREFIX glslang::SPIRV INTERFACE_INCLUDE_DIRECTORIES)
|
||||||
|
target_include_directories(SPIRV SYSTEM INTERFACE "${GLSLANG_PREFIX}/glslang")
|
||||||
|
else()
|
||||||
|
set(SKIP_GLSLANG_INSTALL ON CACHE BOOL "")
|
||||||
|
set(ENABLE_GLSLANG_BINARIES OFF CACHE BOOL "")
|
||||||
|
set(ENABLE_SPVREMAPPER OFF CACHE BOOL "")
|
||||||
|
set(ENABLE_CTEST OFF CACHE BOOL "")
|
||||||
|
set(ENABLE_HLSL OFF CACHE BOOL "")
|
||||||
|
set(BUILD_EXTERNAL OFF CACHE BOOL "")
|
||||||
|
add_subdirectory(glslang)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# sirit
|
||||||
|
add_subdirectory(sirit EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
# VMA
|
# VMA
|
||||||
if(USE_SYSTEM_VMA)
|
if(USE_SYSTEM_VMA)
|
||||||
add_library(vma INTERFACE)
|
add_library(vma INTERFACE)
|
||||||
|
@ -376,12 +413,18 @@ endif()
|
||||||
|
|
||||||
# vulkan-headers
|
# vulkan-headers
|
||||||
add_library(vulkan-headers INTERFACE)
|
add_library(vulkan-headers INTERFACE)
|
||||||
|
if(USE_SYSTEM_VULKAN_HEADERS)
|
||||||
|
find_package(Vulkan REQUIRED)
|
||||||
|
if(TARGET Vulkan::Headers)
|
||||||
|
message(STATUS "Found Vulkan headers")
|
||||||
|
target_link_libraries(vulkan-headers INTERFACE Vulkan::Headers)
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
target_include_directories(vulkan-headers SYSTEM INTERFACE ./vulkan-headers/include)
|
target_include_directories(vulkan-headers SYSTEM INTERFACE ./vulkan-headers/include)
|
||||||
if (APPLE)
|
|
||||||
target_include_directories(vulkan-headers SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/MoltenVK)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# adrenotools
|
# adrenotools
|
||||||
if (ANDROID AND "arm64" IN_LIST ARCHITECTURE)
|
if (ANDROID AND "arm64" IN_LIST ARCHITECTURE)
|
||||||
add_subdirectory(libadrenotools)
|
add_subdirectory(libadrenotools)
|
||||||
endif()
|
endif()
|
||||||
|
endif()
|
||||||
|
|
8
externals/android-ifaddrs/CMakeLists.txt
vendored
8
externals/android-ifaddrs/CMakeLists.txt
vendored
|
@ -1,8 +0,0 @@
|
||||||
add_library(ifaddrs
|
|
||||||
ifaddrs.c
|
|
||||||
ifaddrs.h
|
|
||||||
)
|
|
||||||
|
|
||||||
create_target_directory_groups(ifaddrs)
|
|
||||||
|
|
||||||
target_include_directories(ifaddrs INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
|
|
600
externals/android-ifaddrs/ifaddrs.c
vendored
600
externals/android-ifaddrs/ifaddrs.c
vendored
|
@ -1,600 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright (c) 2013, Kenneth MacKay
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
are permitted provided that the following conditions are met:
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
||||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
||||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ifaddrs.h"
|
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <net/if_arp.h>
|
|
||||||
#include <netinet/in.h>
|
|
||||||
#include <linux/netlink.h>
|
|
||||||
#include <linux/rtnetlink.h>
|
|
||||||
|
|
||||||
typedef struct NetlinkList
|
|
||||||
{
|
|
||||||
struct NetlinkList *m_next;
|
|
||||||
struct nlmsghdr *m_data;
|
|
||||||
unsigned int m_size;
|
|
||||||
} NetlinkList;
|
|
||||||
|
|
||||||
static int netlink_socket(void)
|
|
||||||
{
|
|
||||||
int l_socket = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
|
|
||||||
if(l_socket < 0)
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sockaddr_nl l_addr;
|
|
||||||
memset(&l_addr, 0, sizeof(l_addr));
|
|
||||||
l_addr.nl_family = AF_NETLINK;
|
|
||||||
if(bind(l_socket, (struct sockaddr *)&l_addr, sizeof(l_addr)) < 0)
|
|
||||||
{
|
|
||||||
close(l_socket);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return l_socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int netlink_send(int p_socket, int p_request)
|
|
||||||
{
|
|
||||||
char l_buffer[NLMSG_ALIGN(sizeof(struct nlmsghdr)) + NLMSG_ALIGN(sizeof(struct rtgenmsg))];
|
|
||||||
memset(l_buffer, 0, sizeof(l_buffer));
|
|
||||||
struct nlmsghdr *l_hdr = (struct nlmsghdr *)l_buffer;
|
|
||||||
struct rtgenmsg *l_msg = (struct rtgenmsg *)NLMSG_DATA(l_hdr);
|
|
||||||
|
|
||||||
l_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(*l_msg));
|
|
||||||
l_hdr->nlmsg_type = p_request;
|
|
||||||
l_hdr->nlmsg_flags = NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST;
|
|
||||||
l_hdr->nlmsg_pid = 0;
|
|
||||||
l_hdr->nlmsg_seq = p_socket;
|
|
||||||
l_msg->rtgen_family = AF_UNSPEC;
|
|
||||||
|
|
||||||
struct sockaddr_nl l_addr;
|
|
||||||
memset(&l_addr, 0, sizeof(l_addr));
|
|
||||||
l_addr.nl_family = AF_NETLINK;
|
|
||||||
return (sendto(p_socket, l_hdr, l_hdr->nlmsg_len, 0, (struct sockaddr *)&l_addr, sizeof(l_addr)));
|
|
||||||
}
|
|
||||||
|
|
||||||
static int netlink_recv(int p_socket, void *p_buffer, size_t p_len)
|
|
||||||
{
|
|
||||||
struct msghdr l_msg;
|
|
||||||
struct iovec l_iov = { p_buffer, p_len };
|
|
||||||
struct sockaddr_nl l_addr;
|
|
||||||
int l_result;
|
|
||||||
|
|
||||||
for(;;)
|
|
||||||
{
|
|
||||||
l_msg.msg_name = (void *)&l_addr;
|
|
||||||
l_msg.msg_namelen = sizeof(l_addr);
|
|
||||||
l_msg.msg_iov = &l_iov;
|
|
||||||
l_msg.msg_iovlen = 1;
|
|
||||||
l_msg.msg_control = NULL;
|
|
||||||
l_msg.msg_controllen = 0;
|
|
||||||
l_msg.msg_flags = 0;
|
|
||||||
int l_result = recvmsg(p_socket, &l_msg, 0);
|
|
||||||
|
|
||||||
if(l_result < 0)
|
|
||||||
{
|
|
||||||
if(errno == EINTR)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(l_msg.msg_flags & MSG_TRUNC)
|
|
||||||
{ // buffer was too small
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return l_result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static struct nlmsghdr *getNetlinkResponse(int p_socket, int *p_size, int *p_done)
|
|
||||||
{
|
|
||||||
size_t l_size = 4096;
|
|
||||||
void *l_buffer = NULL;
|
|
||||||
|
|
||||||
for(;;)
|
|
||||||
{
|
|
||||||
free(l_buffer);
|
|
||||||
l_buffer = malloc(l_size);
|
|
||||||
|
|
||||||
int l_read = netlink_recv(p_socket, l_buffer, l_size);
|
|
||||||
*p_size = l_read;
|
|
||||||
if(l_read == -2)
|
|
||||||
{
|
|
||||||
free(l_buffer);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
if(l_read >= 0)
|
|
||||||
{
|
|
||||||
pid_t l_pid = getpid();
|
|
||||||
struct nlmsghdr *l_hdr;
|
|
||||||
for(l_hdr = (struct nlmsghdr *)l_buffer; NLMSG_OK(l_hdr, (unsigned int)l_read); l_hdr = (struct nlmsghdr *)NLMSG_NEXT(l_hdr, l_read))
|
|
||||||
{
|
|
||||||
if((pid_t)l_hdr->nlmsg_pid != l_pid || (int)l_hdr->nlmsg_seq != p_socket)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(l_hdr->nlmsg_type == NLMSG_DONE)
|
|
||||||
{
|
|
||||||
*p_done = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(l_hdr->nlmsg_type == NLMSG_ERROR)
|
|
||||||
{
|
|
||||||
free(l_buffer);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return l_buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
l_size *= 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static NetlinkList *newListItem(struct nlmsghdr *p_data, unsigned int p_size)
|
|
||||||
{
|
|
||||||
NetlinkList *l_item = malloc(sizeof(NetlinkList));
|
|
||||||
l_item->m_next = NULL;
|
|
||||||
l_item->m_data = p_data;
|
|
||||||
l_item->m_size = p_size;
|
|
||||||
return l_item;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void freeResultList(NetlinkList *p_list)
|
|
||||||
{
|
|
||||||
NetlinkList *l_cur;
|
|
||||||
while(p_list)
|
|
||||||
{
|
|
||||||
l_cur = p_list;
|
|
||||||
p_list = p_list->m_next;
|
|
||||||
free(l_cur->m_data);
|
|
||||||
free(l_cur);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static NetlinkList *getResultList(int p_socket, int p_request)
|
|
||||||
{
|
|
||||||
if(netlink_send(p_socket, p_request) < 0)
|
|
||||||
{
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
NetlinkList *l_list = NULL;
|
|
||||||
NetlinkList *l_end = NULL;
|
|
||||||
int l_size;
|
|
||||||
int l_done = 0;
|
|
||||||
while(!l_done)
|
|
||||||
{
|
|
||||||
struct nlmsghdr *l_hdr = getNetlinkResponse(p_socket, &l_size, &l_done);
|
|
||||||
if(!l_hdr)
|
|
||||||
{ // error
|
|
||||||
freeResultList(l_list);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
NetlinkList *l_item = newListItem(l_hdr, l_size);
|
|
||||||
if(!l_list)
|
|
||||||
{
|
|
||||||
l_list = l_item;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
l_end->m_next = l_item;
|
|
||||||
}
|
|
||||||
l_end = l_item;
|
|
||||||
}
|
|
||||||
return l_list;
|
|
||||||
}
|
|
||||||
|
|
||||||
static size_t maxSize(size_t a, size_t b)
|
|
||||||
{
|
|
||||||
return (a > b ? a : b);
|
|
||||||
}
|
|
||||||
|
|
||||||
static size_t calcAddrLen(sa_family_t p_family, int p_dataSize)
|
|
||||||
{
|
|
||||||
switch(p_family)
|
|
||||||
{
|
|
||||||
case AF_INET:
|
|
||||||
return sizeof(struct sockaddr_in);
|
|
||||||
case AF_INET6:
|
|
||||||
return sizeof(struct sockaddr_in6);
|
|
||||||
case AF_PACKET:
|
|
||||||
return maxSize(sizeof(struct sockaddr_ll), offsetof(struct sockaddr_ll, sll_addr) + p_dataSize);
|
|
||||||
default:
|
|
||||||
return maxSize(sizeof(struct sockaddr), offsetof(struct sockaddr, sa_data) + p_dataSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void makeSockaddr(sa_family_t p_family, struct sockaddr *p_dest, void *p_data, size_t p_size)
|
|
||||||
{
|
|
||||||
switch(p_family)
|
|
||||||
{
|
|
||||||
case AF_INET:
|
|
||||||
memcpy(&((struct sockaddr_in*)p_dest)->sin_addr, p_data, p_size);
|
|
||||||
break;
|
|
||||||
case AF_INET6:
|
|
||||||
memcpy(&((struct sockaddr_in6*)p_dest)->sin6_addr, p_data, p_size);
|
|
||||||
break;
|
|
||||||
case AF_PACKET:
|
|
||||||
memcpy(((struct sockaddr_ll*)p_dest)->sll_addr, p_data, p_size);
|
|
||||||
((struct sockaddr_ll*)p_dest)->sll_halen = p_size;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
memcpy(p_dest->sa_data, p_data, p_size);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
p_dest->sa_family = p_family;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void addToEnd(struct ifaddrs **p_resultList, struct ifaddrs *p_entry)
|
|
||||||
{
|
|
||||||
if(!*p_resultList)
|
|
||||||
{
|
|
||||||
*p_resultList = p_entry;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
struct ifaddrs *l_cur = *p_resultList;
|
|
||||||
while(l_cur->ifa_next)
|
|
||||||
{
|
|
||||||
l_cur = l_cur->ifa_next;
|
|
||||||
}
|
|
||||||
l_cur->ifa_next = p_entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void interpretLink(struct nlmsghdr *p_hdr, struct ifaddrs **p_links, struct ifaddrs **p_resultList)
|
|
||||||
{
|
|
||||||
struct ifinfomsg *l_info = (struct ifinfomsg *)NLMSG_DATA(p_hdr);
|
|
||||||
|
|
||||||
size_t l_nameSize = 0;
|
|
||||||
size_t l_addrSize = 0;
|
|
||||||
size_t l_dataSize = 0;
|
|
||||||
|
|
||||||
size_t l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifinfomsg));
|
|
||||||
struct rtattr *l_rta;
|
|
||||||
for(l_rta = (struct rtattr *)(((char *)l_info) + NLMSG_ALIGN(sizeof(struct ifinfomsg))); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))
|
|
||||||
{
|
|
||||||
void *l_rtaData = RTA_DATA(l_rta);
|
|
||||||
size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);
|
|
||||||
switch(l_rta->rta_type)
|
|
||||||
{
|
|
||||||
case IFLA_ADDRESS:
|
|
||||||
case IFLA_BROADCAST:
|
|
||||||
l_addrSize += NLMSG_ALIGN(calcAddrLen(AF_PACKET, l_rtaDataSize));
|
|
||||||
break;
|
|
||||||
case IFLA_IFNAME:
|
|
||||||
l_nameSize += NLMSG_ALIGN(l_rtaSize + 1);
|
|
||||||
break;
|
|
||||||
case IFLA_STATS:
|
|
||||||
l_dataSize += NLMSG_ALIGN(l_rtaSize);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ifaddrs *l_entry = malloc(sizeof(struct ifaddrs) + l_nameSize + l_addrSize + l_dataSize);
|
|
||||||
memset(l_entry, 0, sizeof(struct ifaddrs));
|
|
||||||
l_entry->ifa_name = "";
|
|
||||||
|
|
||||||
char *l_name = ((char *)l_entry) + sizeof(struct ifaddrs);
|
|
||||||
char *l_addr = l_name + l_nameSize;
|
|
||||||
char *l_data = l_addr + l_addrSize;
|
|
||||||
|
|
||||||
l_entry->ifa_flags = l_info->ifi_flags;
|
|
||||||
|
|
||||||
l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifinfomsg));
|
|
||||||
for(l_rta = (struct rtattr *)(((char *)l_info) + NLMSG_ALIGN(sizeof(struct ifinfomsg))); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))
|
|
||||||
{
|
|
||||||
void *l_rtaData = RTA_DATA(l_rta);
|
|
||||||
size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);
|
|
||||||
switch(l_rta->rta_type)
|
|
||||||
{
|
|
||||||
case IFLA_ADDRESS:
|
|
||||||
case IFLA_BROADCAST:
|
|
||||||
{
|
|
||||||
size_t l_addrLen = calcAddrLen(AF_PACKET, l_rtaDataSize);
|
|
||||||
makeSockaddr(AF_PACKET, (struct sockaddr *)l_addr, l_rtaData, l_rtaDataSize);
|
|
||||||
((struct sockaddr_ll *)l_addr)->sll_ifindex = l_info->ifi_index;
|
|
||||||
((struct sockaddr_ll *)l_addr)->sll_hatype = l_info->ifi_type;
|
|
||||||
if(l_rta->rta_type == IFLA_ADDRESS)
|
|
||||||
{
|
|
||||||
l_entry->ifa_addr = (struct sockaddr *)l_addr;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
l_entry->ifa_broadaddr = (struct sockaddr *)l_addr;
|
|
||||||
}
|
|
||||||
l_addr += NLMSG_ALIGN(l_addrLen);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case IFLA_IFNAME:
|
|
||||||
strncpy(l_name, l_rtaData, l_rtaDataSize);
|
|
||||||
l_name[l_rtaDataSize] = '\0';
|
|
||||||
l_entry->ifa_name = l_name;
|
|
||||||
break;
|
|
||||||
case IFLA_STATS:
|
|
||||||
memcpy(l_data, l_rtaData, l_rtaDataSize);
|
|
||||||
l_entry->ifa_data = l_data;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addToEnd(p_resultList, l_entry);
|
|
||||||
p_links[l_info->ifi_index - 1] = l_entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void interpretAddr(struct nlmsghdr *p_hdr, struct ifaddrs **p_links, struct ifaddrs **p_resultList)
|
|
||||||
{
|
|
||||||
struct ifaddrmsg *l_info = (struct ifaddrmsg *)NLMSG_DATA(p_hdr);
|
|
||||||
|
|
||||||
size_t l_nameSize = 0;
|
|
||||||
size_t l_addrSize = 0;
|
|
||||||
|
|
||||||
int l_addedNetmask = 0;
|
|
||||||
|
|
||||||
size_t l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifaddrmsg));
|
|
||||||
struct rtattr *l_rta;
|
|
||||||
for(l_rta = (struct rtattr *)(((char *)l_info) + NLMSG_ALIGN(sizeof(struct ifaddrmsg))); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))
|
|
||||||
{
|
|
||||||
void *l_rtaData = RTA_DATA(l_rta);
|
|
||||||
size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);
|
|
||||||
if(l_info->ifa_family == AF_PACKET)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch(l_rta->rta_type)
|
|
||||||
{
|
|
||||||
case IFA_ADDRESS:
|
|
||||||
case IFA_LOCAL:
|
|
||||||
if((l_info->ifa_family == AF_INET || l_info->ifa_family == AF_INET6) && !l_addedNetmask)
|
|
||||||
{ // make room for netmask
|
|
||||||
l_addrSize += NLMSG_ALIGN(calcAddrLen(l_info->ifa_family, l_rtaDataSize));
|
|
||||||
l_addedNetmask = 1;
|
|
||||||
}
|
|
||||||
case IFA_BROADCAST:
|
|
||||||
l_addrSize += NLMSG_ALIGN(calcAddrLen(l_info->ifa_family, l_rtaDataSize));
|
|
||||||
break;
|
|
||||||
case IFA_LABEL:
|
|
||||||
l_nameSize += NLMSG_ALIGN(l_rtaSize + 1);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ifaddrs *l_entry = malloc(sizeof(struct ifaddrs) + l_nameSize + l_addrSize);
|
|
||||||
memset(l_entry, 0, sizeof(struct ifaddrs));
|
|
||||||
l_entry->ifa_name = p_links[l_info->ifa_index - 1]->ifa_name;
|
|
||||||
|
|
||||||
char *l_name = ((char *)l_entry) + sizeof(struct ifaddrs);
|
|
||||||
char *l_addr = l_name + l_nameSize;
|
|
||||||
|
|
||||||
l_entry->ifa_flags = l_info->ifa_flags | p_links[l_info->ifa_index - 1]->ifa_flags;
|
|
||||||
|
|
||||||
l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifaddrmsg));
|
|
||||||
for(l_rta = (struct rtattr *)(((char *)l_info) + NLMSG_ALIGN(sizeof(struct ifaddrmsg))); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))
|
|
||||||
{
|
|
||||||
void *l_rtaData = RTA_DATA(l_rta);
|
|
||||||
size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);
|
|
||||||
switch(l_rta->rta_type)
|
|
||||||
{
|
|
||||||
case IFA_ADDRESS:
|
|
||||||
case IFA_BROADCAST:
|
|
||||||
case IFA_LOCAL:
|
|
||||||
{
|
|
||||||
size_t l_addrLen = calcAddrLen(l_info->ifa_family, l_rtaDataSize);
|
|
||||||
makeSockaddr(l_info->ifa_family, (struct sockaddr *)l_addr, l_rtaData, l_rtaDataSize);
|
|
||||||
if(l_info->ifa_family == AF_INET6)
|
|
||||||
{
|
|
||||||
if(IN6_IS_ADDR_LINKLOCAL((struct in6_addr *)l_rtaData) || IN6_IS_ADDR_MC_LINKLOCAL((struct in6_addr *)l_rtaData))
|
|
||||||
{
|
|
||||||
((struct sockaddr_in6 *)l_addr)->sin6_scope_id = l_info->ifa_index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(l_rta->rta_type == IFA_ADDRESS)
|
|
||||||
{ // apparently in a point-to-point network IFA_ADDRESS contains the dest address and IFA_LOCAL contains the local address
|
|
||||||
if(l_entry->ifa_addr)
|
|
||||||
{
|
|
||||||
l_entry->ifa_dstaddr = (struct sockaddr *)l_addr;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
l_entry->ifa_addr = (struct sockaddr *)l_addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(l_rta->rta_type == IFA_LOCAL)
|
|
||||||
{
|
|
||||||
if(l_entry->ifa_addr)
|
|
||||||
{
|
|
||||||
l_entry->ifa_dstaddr = l_entry->ifa_addr;
|
|
||||||
}
|
|
||||||
l_entry->ifa_addr = (struct sockaddr *)l_addr;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
l_entry->ifa_broadaddr = (struct sockaddr *)l_addr;
|
|
||||||
}
|
|
||||||
l_addr += NLMSG_ALIGN(l_addrLen);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case IFA_LABEL:
|
|
||||||
strncpy(l_name, l_rtaData, l_rtaDataSize);
|
|
||||||
l_name[l_rtaDataSize] = '\0';
|
|
||||||
l_entry->ifa_name = l_name;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(l_entry->ifa_addr && (l_entry->ifa_addr->sa_family == AF_INET || l_entry->ifa_addr->sa_family == AF_INET6))
|
|
||||||
{
|
|
||||||
unsigned l_maxPrefix = (l_entry->ifa_addr->sa_family == AF_INET ? 32 : 128);
|
|
||||||
unsigned l_prefix = (l_info->ifa_prefixlen > l_maxPrefix ? l_maxPrefix : l_info->ifa_prefixlen);
|
|
||||||
char l_mask[16] = {0};
|
|
||||||
unsigned i;
|
|
||||||
for(i=0; i<(l_prefix/8); ++i)
|
|
||||||
{
|
|
||||||
l_mask[i] = 0xff;
|
|
||||||
}
|
|
||||||
l_mask[i] = 0xff << (8 - (l_prefix % 8));
|
|
||||||
|
|
||||||
makeSockaddr(l_entry->ifa_addr->sa_family, (struct sockaddr *)l_addr, l_mask, l_maxPrefix / 8);
|
|
||||||
l_entry->ifa_netmask = (struct sockaddr *)l_addr;
|
|
||||||
}
|
|
||||||
|
|
||||||
addToEnd(p_resultList, l_entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void interpret(int p_socket, NetlinkList *p_netlinkList, struct ifaddrs **p_links, struct ifaddrs **p_resultList)
|
|
||||||
{
|
|
||||||
pid_t l_pid = getpid();
|
|
||||||
for(; p_netlinkList; p_netlinkList = p_netlinkList->m_next)
|
|
||||||
{
|
|
||||||
unsigned int l_nlsize = p_netlinkList->m_size;
|
|
||||||
struct nlmsghdr *l_hdr;
|
|
||||||
for(l_hdr = p_netlinkList->m_data; NLMSG_OK(l_hdr, l_nlsize); l_hdr = NLMSG_NEXT(l_hdr, l_nlsize))
|
|
||||||
{
|
|
||||||
if((pid_t)l_hdr->nlmsg_pid != l_pid || (int)l_hdr->nlmsg_seq != p_socket)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(l_hdr->nlmsg_type == NLMSG_DONE)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(l_hdr->nlmsg_type == RTM_NEWLINK)
|
|
||||||
{
|
|
||||||
interpretLink(l_hdr, p_links, p_resultList);
|
|
||||||
}
|
|
||||||
else if(l_hdr->nlmsg_type == RTM_NEWADDR)
|
|
||||||
{
|
|
||||||
interpretAddr(l_hdr, p_links, p_resultList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static unsigned countLinks(int p_socket, NetlinkList *p_netlinkList)
|
|
||||||
{
|
|
||||||
unsigned l_links = 0;
|
|
||||||
pid_t l_pid = getpid();
|
|
||||||
for(; p_netlinkList; p_netlinkList = p_netlinkList->m_next)
|
|
||||||
{
|
|
||||||
unsigned int l_nlsize = p_netlinkList->m_size;
|
|
||||||
struct nlmsghdr *l_hdr;
|
|
||||||
for(l_hdr = p_netlinkList->m_data; NLMSG_OK(l_hdr, l_nlsize); l_hdr = NLMSG_NEXT(l_hdr, l_nlsize))
|
|
||||||
{
|
|
||||||
if((pid_t)l_hdr->nlmsg_pid != l_pid || (int)l_hdr->nlmsg_seq != p_socket)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(l_hdr->nlmsg_type == NLMSG_DONE)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(l_hdr->nlmsg_type == RTM_NEWLINK)
|
|
||||||
{
|
|
||||||
++l_links;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return l_links;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getifaddrs(struct ifaddrs **ifap)
|
|
||||||
{
|
|
||||||
if(!ifap)
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
*ifap = NULL;
|
|
||||||
|
|
||||||
int l_socket = netlink_socket();
|
|
||||||
if(l_socket < 0)
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
NetlinkList *l_linkResults = getResultList(l_socket, RTM_GETLINK);
|
|
||||||
if(!l_linkResults)
|
|
||||||
{
|
|
||||||
close(l_socket);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
NetlinkList *l_addrResults = getResultList(l_socket, RTM_GETADDR);
|
|
||||||
if(!l_addrResults)
|
|
||||||
{
|
|
||||||
close(l_socket);
|
|
||||||
freeResultList(l_linkResults);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned l_numLinks = countLinks(l_socket, l_linkResults) + countLinks(l_socket, l_addrResults);
|
|
||||||
struct ifaddrs *l_links[l_numLinks];
|
|
||||||
memset(l_links, 0, l_numLinks * sizeof(struct ifaddrs *));
|
|
||||||
|
|
||||||
interpret(l_socket, l_linkResults, l_links, ifap);
|
|
||||||
interpret(l_socket, l_addrResults, l_links, ifap);
|
|
||||||
|
|
||||||
freeResultList(l_linkResults);
|
|
||||||
freeResultList(l_addrResults);
|
|
||||||
close(l_socket);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void freeifaddrs(struct ifaddrs *ifa)
|
|
||||||
{
|
|
||||||
struct ifaddrs *l_cur;
|
|
||||||
while(ifa)
|
|
||||||
{
|
|
||||||
l_cur = ifa;
|
|
||||||
ifa = ifa->ifa_next;
|
|
||||||
free(l_cur);
|
|
||||||
}
|
|
||||||
}
|
|
54
externals/android-ifaddrs/ifaddrs.h
vendored
54
externals/android-ifaddrs/ifaddrs.h
vendored
|
@ -1,54 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 1995, 1999
|
|
||||||
* Berkeley Software Design, Inc. All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions
|
|
||||||
* are met:
|
|
||||||
* 1. Redistributions of source code must retain the above copyright
|
|
||||||
* notice, this list of conditions and the following disclaimer.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY Berkeley Software Design, Inc. ``AS IS'' AND
|
|
||||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
||||||
* ARE DISCLAIMED. IN NO EVENT SHALL Berkeley Software Design, Inc. BE LIABLE
|
|
||||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
||||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
||||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
||||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
||||||
* SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
* BSDI ifaddrs.h,v 2.5 2000/02/23 14:51:59 dab Exp
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef _IFADDRS_H_
|
|
||||||
#define _IFADDRS_H_
|
|
||||||
|
|
||||||
struct ifaddrs {
|
|
||||||
struct ifaddrs *ifa_next;
|
|
||||||
char *ifa_name;
|
|
||||||
unsigned int ifa_flags;
|
|
||||||
struct sockaddr *ifa_addr;
|
|
||||||
struct sockaddr *ifa_netmask;
|
|
||||||
struct sockaddr *ifa_dstaddr;
|
|
||||||
void *ifa_data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This may have been defined in <net/if.h>. Note that if <net/if.h> is
|
|
||||||
* to be included it must be included before this header file.
|
|
||||||
*/
|
|
||||||
#ifndef ifa_broadaddr
|
|
||||||
#define ifa_broadaddr ifa_dstaddr /* broadcast address interface */
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <sys/cdefs.h>
|
|
||||||
|
|
||||||
__BEGIN_DECLS
|
|
||||||
extern int getifaddrs(struct ifaddrs **ifap);
|
|
||||||
extern void freeifaddrs(struct ifaddrs *ifa);
|
|
||||||
__END_DECLS
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -24,6 +24,8 @@ option(USE_SYSTEM_CUBEB "Use the system cubeb (instead of the bundled one)" OFF)
|
||||||
option(USE_SYSTEM_LODEPNG "Use the system lodepng (instead of the bundled one)" OFF)
|
option(USE_SYSTEM_LODEPNG "Use the system lodepng (instead of the bundled one)" OFF)
|
||||||
option(USE_SYSTEM_OPENAL "Use the system OpenAL (instead of the bundled one)" OFF)
|
option(USE_SYSTEM_OPENAL "Use the system OpenAL (instead of the bundled one)" OFF)
|
||||||
option(USE_SYSTEM_VMA "Use the system VulkanMemoryAllocator (instead of the bundled one)" OFF)
|
option(USE_SYSTEM_VMA "Use the system VulkanMemoryAllocator (instead of the bundled one)" OFF)
|
||||||
|
option(USE_SYSTEM_VULKAN_HEADERS "Use the system Vulkan headers (instead of the bundled ones)" OFF)
|
||||||
|
option(USE_SYSTEM_CATCH2 "Use the system Catch2 (instead of the bundled one)" OFF)
|
||||||
|
|
||||||
# Qt and MoltenVK are handled separately
|
# Qt and MoltenVK are handled separately
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_SDL2 "Disable system SDL2" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_SDL2 "Disable system SDL2" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
|
@ -47,6 +49,8 @@ CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CUBEB "Disable system cubeb" OFF "USE_SYST
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_LODEPNG "Disable system lodepng" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_LODEPNG "Disable system lodepng" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_OPENAL "Disable system OpenAL" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_OPENAL "Disable system OpenAL" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_VMA "Disable system VulkanMemoryAllocator" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_VMA "Disable system VulkanMemoryAllocator" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_VULKAN_HEADERS "Disable system Vulkan headers" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CATCH2 "Disable system Catch2" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
|
|
||||||
set(LIB_VAR_LIST
|
set(LIB_VAR_LIST
|
||||||
SDL2
|
SDL2
|
||||||
|
@ -70,6 +74,8 @@ set(LIB_VAR_LIST
|
||||||
LODEPNG
|
LODEPNG
|
||||||
OPENAL
|
OPENAL
|
||||||
VMA
|
VMA
|
||||||
|
VULKAN_HEADERS
|
||||||
|
CATCH2
|
||||||
)
|
)
|
||||||
|
|
||||||
# First, check that USE_SYSTEM_XXX is not used with USE_SYSTEM_LIBS
|
# First, check that USE_SYSTEM_XXX is not used with USE_SYSTEM_LIBS
|
||||||
|
|
2
externals/cmake-modules/FindOpenAL.cmake
vendored
2
externals/cmake-modules/FindOpenAL.cmake
vendored
|
@ -1,5 +1,5 @@
|
||||||
if(NOT OPENAL_FOUND)
|
if(NOT OPENAL_FOUND)
|
||||||
pkg_check_modules(OOPENAL_TMP libopanal)
|
pkg_check_modules(OPENAL_TMP openal)
|
||||||
|
|
||||||
find_path(OPENAL_INCLUDE_DIRS NAMES al.h
|
find_path(OPENAL_INCLUDE_DIRS NAMES al.h
|
||||||
PATHS
|
PATHS
|
||||||
|
|
9
externals/cmake-modules/Findcryptopp.cmake
vendored
9
externals/cmake-modules/Findcryptopp.cmake
vendored
|
@ -1,20 +1,19 @@
|
||||||
if(NOT CRYPTOPP_FOUND)
|
if(NOT CRYPTOPP_FOUND)
|
||||||
pkg_check_modules(CRYPTOPP_TMP libcrypto++)
|
pkg_search_module(CRYPTOPP_TMP crypto++ cryptopp)
|
||||||
|
|
||||||
find_path(CRYPTOPP_INCLUDE_DIRS NAMES cryptlib.h
|
find_path(CRYPTOPP_INCLUDE_DIRS NAMES cryptlib.h
|
||||||
PATHS
|
PATHS
|
||||||
${CRYPTOPP_TMP_INCLUDE_DIRS}
|
${CRYPTOPP_TMP_INCLUDE_DIRS}
|
||||||
/usr/include
|
/usr/include
|
||||||
/usr/include/crypto++
|
|
||||||
/usr/local/include
|
/usr/local/include
|
||||||
/usr/local/include/crypto++
|
PATH_SUFFIXES crypto++ cryptopp
|
||||||
)
|
)
|
||||||
|
|
||||||
find_library(CRYPTOPP_LIBRARY_DIRS NAMES crypto++
|
find_library(CRYPTOPP_LIBRARY_DIRS NAMES crypto++ cryptopp
|
||||||
PATHS
|
PATHS
|
||||||
${CRYPTOPP_TMP_LIBRARY_DIRS}
|
${CRYPTOPP_TMP_LIBRARY_DIRS}
|
||||||
/usr/lib
|
/usr/lib
|
||||||
/usr/locallib
|
/usr/local/lib
|
||||||
)
|
)
|
||||||
|
|
||||||
if(CRYPTOPP_INCLUDE_DIRS AND CRYPTOPP_LIBRARY_DIRS)
|
if(CRYPTOPP_INCLUDE_DIRS AND CRYPTOPP_LIBRARY_DIRS)
|
||||||
|
|
2
externals/cryptopp-cmake
vendored
2
externals/cryptopp-cmake
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 9327192b0095dc1f420b2082d37bd427b5750d48
|
Subproject commit a99c80c26686e44eddf0432140ae397f3efbd0b3
|
2
externals/cubeb
vendored
2
externals/cubeb
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 48689ae7a73caeb747953f9ed664dc71d2f918d8
|
Subproject commit 799e775484b8fce7e986ee7a4f4b651fec2bca07
|
2
externals/dynarmic
vendored
2
externals/dynarmic
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit d333a09b3b9152af3cb442902ae8ea18d8416470
|
Subproject commit 30f1a3c6289075ef4af08f5ec502be2fc8627a0c
|
9
externals/gamemode/CMakeLists.txt
vendored
Normal file
9
externals/gamemode/CMakeLists.txt
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
project(gamemode LANGUAGES CXX C)
|
||||||
|
|
||||||
|
add_library(gamemode include/gamemode_client.h)
|
||||||
|
|
||||||
|
target_include_directories(gamemode PUBLIC include)
|
||||||
|
set_target_properties(gamemode PROPERTIES LINKER_LANGUAGE C)
|
379
externals/gamemode/include/gamemode_client.h
vendored
Normal file
379
externals/gamemode/include/gamemode_client.h
vendored
Normal file
|
@ -0,0 +1,379 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2017-2019 Feral Interactive
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Copyright (c) 2017-2019, Feral Interactive
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of Feral Interactive nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
#ifndef CLIENT_GAMEMODE_H
|
||||||
|
#define CLIENT_GAMEMODE_H
|
||||||
|
/*
|
||||||
|
* GameMode supports the following client functions
|
||||||
|
* Requests are refcounted in the daemon
|
||||||
|
*
|
||||||
|
* int gamemode_request_start() - Request gamemode starts
|
||||||
|
* 0 if the request was sent successfully
|
||||||
|
* -1 if the request failed
|
||||||
|
*
|
||||||
|
* int gamemode_request_end() - Request gamemode ends
|
||||||
|
* 0 if the request was sent successfully
|
||||||
|
* -1 if the request failed
|
||||||
|
*
|
||||||
|
* GAMEMODE_AUTO can be defined to make the above two functions apply during static init and
|
||||||
|
* destruction, as appropriate. In this configuration, errors will be printed to stderr
|
||||||
|
*
|
||||||
|
* int gamemode_query_status() - Query the current status of gamemode
|
||||||
|
* 0 if gamemode is inactive
|
||||||
|
* 1 if gamemode is active
|
||||||
|
* 2 if gamemode is active and this client is registered
|
||||||
|
* -1 if the query failed
|
||||||
|
*
|
||||||
|
* int gamemode_request_start_for(pid_t pid) - Request gamemode starts for another process
|
||||||
|
* 0 if the request was sent successfully
|
||||||
|
* -1 if the request failed
|
||||||
|
* -2 if the request was rejected
|
||||||
|
*
|
||||||
|
* int gamemode_request_end_for(pid_t pid) - Request gamemode ends for another process
|
||||||
|
* 0 if the request was sent successfully
|
||||||
|
* -1 if the request failed
|
||||||
|
* -2 if the request was rejected
|
||||||
|
*
|
||||||
|
* int gamemode_query_status_for(pid_t pid) - Query status of gamemode for another process
|
||||||
|
* 0 if gamemode is inactive
|
||||||
|
* 1 if gamemode is active
|
||||||
|
* 2 if gamemode is active and this client is registered
|
||||||
|
* -1 if the query failed
|
||||||
|
*
|
||||||
|
* const char* gamemode_error_string() - Get an error string
|
||||||
|
* returns a string describing any of the above errors
|
||||||
|
*
|
||||||
|
* Note: All the above requests can be blocking - dbus requests can and will block while the daemon
|
||||||
|
* handles the request. It is not recommended to make these calls in performance critical code
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
static char internal_gamemode_client_error_string[512] = { 0 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load libgamemode dynamically to dislodge us from most dependencies.
|
||||||
|
* This allows clients to link and/or use this regardless of runtime.
|
||||||
|
* See SDL2 for an example of the reasoning behind this in terms of
|
||||||
|
* dynamic versioning as well.
|
||||||
|
*/
|
||||||
|
static volatile int internal_libgamemode_loaded = 1;
|
||||||
|
|
||||||
|
/* Typedefs for the functions to load */
|
||||||
|
typedef int (*api_call_return_int)(void);
|
||||||
|
typedef const char *(*api_call_return_cstring)(void);
|
||||||
|
typedef int (*api_call_pid_return_int)(pid_t);
|
||||||
|
|
||||||
|
/* Storage for functors */
|
||||||
|
static api_call_return_int REAL_internal_gamemode_request_start = NULL;
|
||||||
|
static api_call_return_int REAL_internal_gamemode_request_end = NULL;
|
||||||
|
static api_call_return_int REAL_internal_gamemode_query_status = NULL;
|
||||||
|
static api_call_return_cstring REAL_internal_gamemode_error_string = NULL;
|
||||||
|
static api_call_pid_return_int REAL_internal_gamemode_request_start_for = NULL;
|
||||||
|
static api_call_pid_return_int REAL_internal_gamemode_request_end_for = NULL;
|
||||||
|
static api_call_pid_return_int REAL_internal_gamemode_query_status_for = NULL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal helper to perform the symbol binding safely.
|
||||||
|
*
|
||||||
|
* Returns 0 on success and -1 on failure
|
||||||
|
*/
|
||||||
|
__attribute__((always_inline)) static inline int internal_bind_libgamemode_symbol(
|
||||||
|
void *handle, const char *name, void **out_func, size_t func_size, bool required)
|
||||||
|
{
|
||||||
|
void *symbol_lookup = NULL;
|
||||||
|
char *dl_error = NULL;
|
||||||
|
|
||||||
|
/* Safely look up the symbol */
|
||||||
|
symbol_lookup = dlsym(handle, name);
|
||||||
|
dl_error = dlerror();
|
||||||
|
if (required && (dl_error || !symbol_lookup)) {
|
||||||
|
snprintf(internal_gamemode_client_error_string,
|
||||||
|
sizeof(internal_gamemode_client_error_string),
|
||||||
|
"dlsym failed - %s",
|
||||||
|
dl_error);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Have the symbol correctly, copy it to make it usable */
|
||||||
|
memcpy(out_func, &symbol_lookup, func_size);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads libgamemode and needed functions
|
||||||
|
*
|
||||||
|
* Returns 0 on success and -1 on failure
|
||||||
|
*/
|
||||||
|
__attribute__((always_inline)) static inline int internal_load_libgamemode(void)
|
||||||
|
{
|
||||||
|
/* We start at 1, 0 is a success and -1 is a fail */
|
||||||
|
if (internal_libgamemode_loaded != 1) {
|
||||||
|
return internal_libgamemode_loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anonymous struct type to define our bindings */
|
||||||
|
struct binding {
|
||||||
|
const char *name;
|
||||||
|
void **functor;
|
||||||
|
size_t func_size;
|
||||||
|
bool required;
|
||||||
|
} bindings[] = {
|
||||||
|
{ "real_gamemode_request_start",
|
||||||
|
(void **)&REAL_internal_gamemode_request_start,
|
||||||
|
sizeof(REAL_internal_gamemode_request_start),
|
||||||
|
true },
|
||||||
|
{ "real_gamemode_request_end",
|
||||||
|
(void **)&REAL_internal_gamemode_request_end,
|
||||||
|
sizeof(REAL_internal_gamemode_request_end),
|
||||||
|
true },
|
||||||
|
{ "real_gamemode_query_status",
|
||||||
|
(void **)&REAL_internal_gamemode_query_status,
|
||||||
|
sizeof(REAL_internal_gamemode_query_status),
|
||||||
|
false },
|
||||||
|
{ "real_gamemode_error_string",
|
||||||
|
(void **)&REAL_internal_gamemode_error_string,
|
||||||
|
sizeof(REAL_internal_gamemode_error_string),
|
||||||
|
true },
|
||||||
|
{ "real_gamemode_request_start_for",
|
||||||
|
(void **)&REAL_internal_gamemode_request_start_for,
|
||||||
|
sizeof(REAL_internal_gamemode_request_start_for),
|
||||||
|
false },
|
||||||
|
{ "real_gamemode_request_end_for",
|
||||||
|
(void **)&REAL_internal_gamemode_request_end_for,
|
||||||
|
sizeof(REAL_internal_gamemode_request_end_for),
|
||||||
|
false },
|
||||||
|
{ "real_gamemode_query_status_for",
|
||||||
|
(void **)&REAL_internal_gamemode_query_status_for,
|
||||||
|
sizeof(REAL_internal_gamemode_query_status_for),
|
||||||
|
false },
|
||||||
|
};
|
||||||
|
|
||||||
|
void *libgamemode = NULL;
|
||||||
|
|
||||||
|
/* Try and load libgamemode */
|
||||||
|
libgamemode = dlopen("libgamemode.so.0", RTLD_NOW);
|
||||||
|
if (!libgamemode) {
|
||||||
|
/* Attempt to load unversioned library for compatibility with older
|
||||||
|
* versions (as of writing, there are no ABI changes between the two -
|
||||||
|
* this may need to change if ever ABI-breaking changes are made) */
|
||||||
|
libgamemode = dlopen("libgamemode.so", RTLD_NOW);
|
||||||
|
if (!libgamemode) {
|
||||||
|
snprintf(internal_gamemode_client_error_string,
|
||||||
|
sizeof(internal_gamemode_client_error_string),
|
||||||
|
"dlopen failed - %s",
|
||||||
|
dlerror());
|
||||||
|
internal_libgamemode_loaded = -1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attempt to bind all symbols */
|
||||||
|
for (size_t i = 0; i < sizeof(bindings) / sizeof(bindings[0]); i++) {
|
||||||
|
struct binding *binder = &bindings[i];
|
||||||
|
|
||||||
|
if (internal_bind_libgamemode_symbol(libgamemode,
|
||||||
|
binder->name,
|
||||||
|
binder->functor,
|
||||||
|
binder->func_size,
|
||||||
|
binder->required)) {
|
||||||
|
internal_libgamemode_loaded = -1;
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success */
|
||||||
|
internal_libgamemode_loaded = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the real libgamemode
|
||||||
|
*/
|
||||||
|
__attribute__((always_inline)) static inline const char *gamemode_error_string(void)
|
||||||
|
{
|
||||||
|
/* If we fail to load the system gamemode, or we have an error string already, return our error
|
||||||
|
* string instead of diverting to the system version */
|
||||||
|
if (internal_load_libgamemode() < 0 || internal_gamemode_client_error_string[0] != '\0') {
|
||||||
|
return internal_gamemode_client_error_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Assert for static analyser that the function is not NULL */
|
||||||
|
assert(REAL_internal_gamemode_error_string != NULL);
|
||||||
|
|
||||||
|
return REAL_internal_gamemode_error_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the real libgamemode
|
||||||
|
* Allow automatically requesting game mode
|
||||||
|
* Also prints errors as they happen.
|
||||||
|
*/
|
||||||
|
#ifdef GAMEMODE_AUTO
|
||||||
|
__attribute__((constructor))
|
||||||
|
#else
|
||||||
|
__attribute__((always_inline)) static inline
|
||||||
|
#endif
|
||||||
|
int gamemode_request_start(void)
|
||||||
|
{
|
||||||
|
/* Need to load gamemode */
|
||||||
|
if (internal_load_libgamemode() < 0) {
|
||||||
|
#ifdef GAMEMODE_AUTO
|
||||||
|
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
|
||||||
|
#endif
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Assert for static analyser that the function is not NULL */
|
||||||
|
assert(REAL_internal_gamemode_request_start != NULL);
|
||||||
|
|
||||||
|
if (REAL_internal_gamemode_request_start() < 0) {
|
||||||
|
#ifdef GAMEMODE_AUTO
|
||||||
|
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
|
||||||
|
#endif
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Redirect to the real libgamemode */
|
||||||
|
#ifdef GAMEMODE_AUTO
|
||||||
|
__attribute__((destructor))
|
||||||
|
#else
|
||||||
|
__attribute__((always_inline)) static inline
|
||||||
|
#endif
|
||||||
|
int gamemode_request_end(void)
|
||||||
|
{
|
||||||
|
/* Need to load gamemode */
|
||||||
|
if (internal_load_libgamemode() < 0) {
|
||||||
|
#ifdef GAMEMODE_AUTO
|
||||||
|
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
|
||||||
|
#endif
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Assert for static analyser that the function is not NULL */
|
||||||
|
assert(REAL_internal_gamemode_request_end != NULL);
|
||||||
|
|
||||||
|
if (REAL_internal_gamemode_request_end() < 0) {
|
||||||
|
#ifdef GAMEMODE_AUTO
|
||||||
|
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
|
||||||
|
#endif
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Redirect to the real libgamemode */
|
||||||
|
__attribute__((always_inline)) static inline int gamemode_query_status(void)
|
||||||
|
{
|
||||||
|
/* Need to load gamemode */
|
||||||
|
if (internal_load_libgamemode() < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (REAL_internal_gamemode_query_status == NULL) {
|
||||||
|
snprintf(internal_gamemode_client_error_string,
|
||||||
|
sizeof(internal_gamemode_client_error_string),
|
||||||
|
"gamemode_query_status missing (older host?)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return REAL_internal_gamemode_query_status();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Redirect to the real libgamemode */
|
||||||
|
__attribute__((always_inline)) static inline int gamemode_request_start_for(pid_t pid)
|
||||||
|
{
|
||||||
|
/* Need to load gamemode */
|
||||||
|
if (internal_load_libgamemode() < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (REAL_internal_gamemode_request_start_for == NULL) {
|
||||||
|
snprintf(internal_gamemode_client_error_string,
|
||||||
|
sizeof(internal_gamemode_client_error_string),
|
||||||
|
"gamemode_request_start_for missing (older host?)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return REAL_internal_gamemode_request_start_for(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Redirect to the real libgamemode */
|
||||||
|
__attribute__((always_inline)) static inline int gamemode_request_end_for(pid_t pid)
|
||||||
|
{
|
||||||
|
/* Need to load gamemode */
|
||||||
|
if (internal_load_libgamemode() < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (REAL_internal_gamemode_request_end_for == NULL) {
|
||||||
|
snprintf(internal_gamemode_client_error_string,
|
||||||
|
sizeof(internal_gamemode_client_error_string),
|
||||||
|
"gamemode_request_end_for missing (older host?)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return REAL_internal_gamemode_request_end_for(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Redirect to the real libgamemode */
|
||||||
|
__attribute__((always_inline)) static inline int gamemode_query_status_for(pid_t pid)
|
||||||
|
{
|
||||||
|
/* Need to load gamemode */
|
||||||
|
if (internal_load_libgamemode() < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (REAL_internal_gamemode_query_status_for == NULL) {
|
||||||
|
snprintf(internal_gamemode_client_error_string,
|
||||||
|
sizeof(internal_gamemode_client_error_string),
|
||||||
|
"gamemode_query_status_for missing (older host?)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return REAL_internal_gamemode_query_status_for(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // CLIENT_GAMEMODE_H
|
1071
externals/moltenvk/mvk_config.h
vendored
1071
externals/moltenvk/mvk_config.h
vendored
File diff suppressed because it is too large
Load diff
2
externals/oaknut
vendored
2
externals/oaknut
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit e6eecc3f9460728be0a8d3f63e66d31c0362f472
|
Subproject commit 6b1d57ea7ed4882d32a91eeaa6557b0ecb4da152
|
2
externals/vulkan-headers
vendored
2
externals/vulkan-headers
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 85c2334e92e215cce34e8e0ed8b2dce4700f4a50
|
Subproject commit 217e93c664ec6704ec2d8c36fa116c1a4a1e2d40
|
BIN
keys.tar.enc
BIN
keys.tar.enc
Binary file not shown.
|
@ -51,6 +51,10 @@ if (MSVC)
|
||||||
/Zc:throwingNew
|
/Zc:throwingNew
|
||||||
/GT
|
/GT
|
||||||
|
|
||||||
|
# Some flags for more deterministic builds, to aid caching.
|
||||||
|
/experimental:deterministic
|
||||||
|
/d1trimfile:"${CMAKE_SOURCE_DIR}"
|
||||||
|
|
||||||
# External headers diagnostics
|
# External headers diagnostics
|
||||||
/experimental:external # Enables the external headers options. This option isn't required in Visual Studio 2019 version 16.10 and later
|
/experimental:external # Enables the external headers options. This option isn't required in Visual Studio 2019 version 16.10 and later
|
||||||
/external:anglebrackets # Treats all headers included by #include <header>, where the header file is enclosed in angle brackets (< >), as external headers
|
/external:anglebrackets # Treats all headers included by #include <header>, where the header file is enclosed in angle brackets (< >), as external headers
|
||||||
|
@ -87,7 +91,8 @@ if (MSVC)
|
||||||
|
|
||||||
# Since MSVC's debugging information is not very deterministic, so we have to disable it
|
# Since MSVC's debugging information is not very deterministic, so we have to disable it
|
||||||
# when using ccache or other caching tools
|
# when using ccache or other caching tools
|
||||||
if (CITRA_USE_CCACHE OR CITRA_USE_PRECOMPILED_HEADERS)
|
if (CMAKE_C_COMPILER_LAUNCHER STREQUAL "ccache" OR CMAKE_CXX_COMPILER_LAUNCHER STREQUAL "ccache"
|
||||||
|
OR CITRA_USE_PRECOMPILED_HEADERS)
|
||||||
# Precompiled headers are deleted if not using /Z7. See https://github.com/nanoant/CMakePCHCompiler/issues/21
|
# Precompiled headers are deleted if not using /Z7. See https://github.com/nanoant/CMakePCHCompiler/issues/21
|
||||||
add_compile_options(/Z7)
|
add_compile_options(/Z7)
|
||||||
else()
|
else()
|
||||||
|
@ -98,19 +103,23 @@ if (MSVC)
|
||||||
add_compile_options("$<$<CONFIG:Release>:/GS->")
|
add_compile_options("$<$<CONFIG:Release>:/GS->")
|
||||||
|
|
||||||
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "/DEBUG /MANIFEST:NO" CACHE STRING "" FORCE)
|
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "/DEBUG /MANIFEST:NO" CACHE STRING "" FORCE)
|
||||||
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/DEBUG /MANIFEST:NO /INCREMENTAL:NO /OPT:REF,ICF" CACHE STRING "" FORCE)
|
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/DEBUG /MANIFEST:NO /INCREMENTAL:NO /OPT:REF,ICF /PDBALTPATH:%_PDB%" CACHE STRING "" FORCE)
|
||||||
else()
|
else()
|
||||||
add_compile_options(
|
add_compile_options(
|
||||||
-Wall
|
-Wall
|
||||||
# In case a flag isn't supported on e.g. a certain architecture, don't error.
|
# In case a flag isn't supported on e.g. a certain architecture, don't error.
|
||||||
-Wno-unused-command-line-argument
|
-Wno-unused-command-line-argument
|
||||||
# Build fortification options
|
# Build fortification options
|
||||||
-Wp,-D_FORTIFY_SOURCE=2
|
|
||||||
-Wp,-D_GLIBCXX_ASSERTIONS
|
-Wp,-D_GLIBCXX_ASSERTIONS
|
||||||
-fstack-protector-strong
|
-fstack-protector-strong
|
||||||
-fstack-clash-protection
|
-fstack-clash-protection
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (NOT CMAKE_BUILD_TYPE STREQUAL Debug)
|
||||||
|
# _FORTIFY_SOURCE can't be used without optimizations.
|
||||||
|
add_compile_options(-Wp,-D_FORTIFY_SOURCE=2)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (CITRA_WARNINGS_AS_ERRORS)
|
if (CITRA_WARNINGS_AS_ERRORS)
|
||||||
add_compile_options(-Werror)
|
add_compile_options(-Werror)
|
||||||
endif()
|
endif()
|
||||||
|
@ -119,6 +128,13 @@ else()
|
||||||
add_compile_options("-stdlib=libc++")
|
add_compile_options("-stdlib=libc++")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (CMAKE_CXX_COMPILER_ID STREQUAL GNU)
|
||||||
|
# GCC may warn when it ignores attributes like maybe_unused,
|
||||||
|
# which is a problem for older versions (e.g. GCC 11).
|
||||||
|
add_compile_options("-Wno-attributes")
|
||||||
|
add_compile_options("-Wno-interference-size")
|
||||||
|
endif()
|
||||||
|
|
||||||
if (MINGW)
|
if (MINGW)
|
||||||
add_definitions(-DMINGW_HAS_SECURE_API)
|
add_definitions(-DMINGW_HAS_SECURE_API)
|
||||||
if (COMPILE_WITH_DWARF)
|
if (COMPILE_WITH_DWARF)
|
||||||
|
@ -143,6 +159,16 @@ else()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(ENABLE_SOFTWARE_RENDERER)
|
||||||
|
add_compile_definitions(ENABLE_SOFTWARE_RENDERER)
|
||||||
|
endif()
|
||||||
|
if(ENABLE_OPENGL)
|
||||||
|
add_compile_definitions(ENABLE_OPENGL)
|
||||||
|
endif()
|
||||||
|
if(ENABLE_VULKAN)
|
||||||
|
add_compile_definitions(ENABLE_VULKAN)
|
||||||
|
endif()
|
||||||
|
|
||||||
add_subdirectory(common)
|
add_subdirectory(common)
|
||||||
add_subdirectory(core)
|
add_subdirectory(core)
|
||||||
add_subdirectory(video_core)
|
add_subdirectory(video_core)
|
||||||
|
|
|
@ -10,7 +10,7 @@ plugins {
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("de.undercouch.download") version "5.5.0"
|
id("de.undercouch.download") version "5.5.0"
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
kotlin("plugin.serialization") version "1.8.21"
|
kotlin("plugin.serialization") version "1.9.22"
|
||||||
id("androidx.navigation.safeargs.kotlin")
|
id("androidx.navigation.safeargs.kotlin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ android {
|
||||||
namespace = "org.citra.citra_emu"
|
namespace = "org.citra.citra_emu"
|
||||||
|
|
||||||
compileSdkVersion = "android-34"
|
compileSdkVersion = "android-34"
|
||||||
ndkVersion = "25.2.9519653"
|
ndkVersion = "26.1.10909125"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
@ -40,6 +40,10 @@ android {
|
||||||
jvmTarget = "17"
|
jvmTarget = "17"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidResources {
|
||||||
|
generateLocaleConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
// This is necessary for libadrenotools custom driver loading
|
// This is necessary for libadrenotools custom driver loading
|
||||||
jniLibs.useLegacyPackaging = true
|
jniLibs.useLegacyPackaging = true
|
||||||
|
@ -169,27 +173,23 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||||
implementation("androidx.activity:activity-ktx:1.8.0")
|
implementation("androidx.activity:activity-ktx:1.8.2")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
||||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.documentfile:documentfile:1.0.1")
|
implementation("androidx.documentfile:documentfile:1.0.1")
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
|
||||||
implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
|
implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
|
||||||
implementation("com.google.android.material:material:1.9.0")
|
implementation("com.google.android.material:material:1.9.0")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
implementation("androidx.work:work-runtime:2.8.1")
|
implementation("androidx.work:work-runtime:2.9.0")
|
||||||
|
|
||||||
// For loading huge screenshots from the disk.
|
|
||||||
implementation("com.squareup.picasso:picasso:2.71828")
|
|
||||||
|
|
||||||
implementation("org.ini4j:ini4j:0.5.4")
|
implementation("org.ini4j:ini4j:0.5.4")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:2.7.5")
|
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:2.7.5")
|
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
|
||||||
implementation("info.debatty:java-string-similarity:2.0.0")
|
implementation("info.debatty:java-string-similarity:2.0.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
|
||||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
implementation("io.coil-kt:coil:2.2.2")
|
implementation("io.coil-kt:coil:2.5.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download Vulkan Validation Layers from the KhronosGroup GitHub.
|
// Download Vulkan Validation Layers from the KhronosGroup GitHub.
|
||||||
|
|
|
@ -42,6 +42,9 @@
|
||||||
android:banner="@mipmap/ic_launcher"
|
android:banner="@mipmap/ic_launcher"
|
||||||
android:requestLegacyExternalStorage="true">
|
android:requestLegacyExternalStorage="true">
|
||||||
|
|
||||||
|
<meta-data android:name="android.game_mode_config"
|
||||||
|
android:resource="@xml/game_mode_config" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.citra.citra_emu.ui.main.MainActivity"
|
android:name="org.citra.citra_emu.ui.main.MainActivity"
|
||||||
android:theme="@style/Theme.Citra.Splash.Main"
|
android:theme="@style/Theme.Citra.Splash.Main"
|
||||||
|
@ -64,9 +67,18 @@
|
||||||
<activity
|
<activity
|
||||||
android:name="org.citra.citra_emu.activities.EmulationActivity"
|
android:name="org.citra.citra_emu.activities.EmulationActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:resizeableActivity="false"
|
|
||||||
android:theme="@style/Theme.Citra.Main"
|
android:theme="@style/Theme.Citra.Main"
|
||||||
android:launchMode="singleTop"/>
|
android:launchMode="singleTop">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data
|
||||||
|
android:mimeType="application/octet-stream"
|
||||||
|
android:scheme="content" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
<service android:name="org.citra.citra_emu.utils.ForegroundService" android:foregroundServiceType="specialUse">
|
<service android:name="org.citra.citra_emu.utils.ForegroundService" android:foregroundServiceType="specialUse">
|
||||||
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Keep emulation running in background"/>
|
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Keep emulation running in background"/>
|
||||||
|
|
|
@ -9,10 +9,13 @@ import android.app.Application
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization
|
import org.citra.citra_emu.utils.DirectoryInitialization
|
||||||
import org.citra.citra_emu.utils.DocumentsTree
|
import org.citra.citra_emu.utils.DocumentsTree
|
||||||
import org.citra.citra_emu.utils.GpuDriverHelper
|
import org.citra.citra_emu.utils.GpuDriverHelper
|
||||||
import org.citra.citra_emu.utils.PermissionsHandler
|
import org.citra.citra_emu.utils.PermissionsHandler
|
||||||
|
import org.citra.citra_emu.utils.Log
|
||||||
|
import org.citra.citra_emu.utils.MemoryUtil
|
||||||
|
|
||||||
class CitraApplication : Application() {
|
class CitraApplication : Application() {
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
|
@ -53,9 +56,20 @@ class CitraApplication : Application() {
|
||||||
}
|
}
|
||||||
|
|
||||||
NativeLibrary.logDeviceInfo()
|
NativeLibrary.logDeviceInfo()
|
||||||
|
logDeviceInfo()
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun logDeviceInfo() {
|
||||||
|
Log.info("Device Manufacturer - ${Build.MANUFACTURER}")
|
||||||
|
Log.info("Device Model - ${Build.MODEL}")
|
||||||
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
|
||||||
|
Log.info("SoC Manufacturer - ${Build.SOC_MANUFACTURER}")
|
||||||
|
Log.info("SoC Model - ${Build.SOC_MODEL}")
|
||||||
|
}
|
||||||
|
Log.info("Total System Memory - ${MemoryUtil.getDeviceRAM()}")
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var application: CitraApplication? = null
|
private var application: CitraApplication? = null
|
||||||
|
|
||||||
|
|
|
@ -252,7 +252,7 @@ object NativeLibrary {
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun landscapeScreenLayout(): Int = EmulationMenuSettings.getLandscapeScreenLayout()
|
fun landscapeScreenLayout(): Int = EmulationMenuSettings.landscapeScreenLayout
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
@ -413,12 +413,12 @@ object NativeLibrary {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setEmulationActivity(emulationActivity: EmulationActivity?) {
|
fun setEmulationActivity(emulationActivity: EmulationActivity?) {
|
||||||
Log.verbose("[NativeLibrary] Registering EmulationActivity.")
|
Log.debug("[NativeLibrary] Registering EmulationActivity.")
|
||||||
sEmulationActivity = WeakReference(emulationActivity)
|
sEmulationActivity = WeakReference(emulationActivity)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearEmulationActivity() {
|
fun clearEmulationActivity() {
|
||||||
Log.verbose("[NativeLibrary] Unregistering EmulationActivity.")
|
Log.debug("[NativeLibrary] Unregistering EmulationActivity.")
|
||||||
sEmulationActivity.clear()
|
sEmulationActivity.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -515,14 +515,6 @@ object NativeLibrary {
|
||||||
*/
|
*/
|
||||||
external fun logDeviceInfo()
|
external fun logDeviceInfo()
|
||||||
|
|
||||||
external fun loadSystemConfig()
|
|
||||||
|
|
||||||
external fun saveSystemConfig()
|
|
||||||
|
|
||||||
external fun setSystemSetupNeeded(needed: Boolean)
|
|
||||||
|
|
||||||
external fun getIsSystemSetupNeeded(): Boolean
|
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun createFile(directory: String, filename: String): Boolean =
|
fun createFile(directory: String, filename: String): Boolean =
|
||||||
|
|
|
@ -1,788 +0,0 @@
|
||||||
package org.citra.citra_emu.activities;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.util.Pair;
|
|
||||||
import android.util.SparseIntArray;
|
|
||||||
import android.view.InputDevice;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.SubMenu;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultCallback;
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.annotation.IntDef;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.widget.PopupMenu;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.CitraApplication;
|
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.contracts.OpenFileResultContract;
|
|
||||||
import org.citra.citra_emu.features.cheats.ui.CheatsActivity;
|
|
||||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
|
||||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
|
|
||||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
|
||||||
import org.citra.citra_emu.camera.StillImageCameraHelper;
|
|
||||||
import org.citra.citra_emu.fragments.EmulationFragment;
|
|
||||||
import org.citra.citra_emu.ui.main.MainActivity;
|
|
||||||
import org.citra.citra_emu.utils.ControllerMappingHelper;
|
|
||||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
|
||||||
import org.citra.citra_emu.utils.FileBrowserHelper;
|
|
||||||
import org.citra.citra_emu.utils.FileUtil;
|
|
||||||
import org.citra.citra_emu.utils.ForegroundService;
|
|
||||||
import org.citra.citra_emu.utils.Log;
|
|
||||||
import org.citra.citra_emu.utils.ThemeUtil;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static android.Manifest.permission.CAMERA;
|
|
||||||
import static android.Manifest.permission.RECORD_AUDIO;
|
|
||||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
||||||
import com.google.android.material.slider.Slider;
|
|
||||||
|
|
||||||
public final class EmulationActivity extends AppCompatActivity {
|
|
||||||
public static final String EXTRA_SELECTED_GAME = "SelectedGame";
|
|
||||||
public static final String EXTRA_SELECTED_TITLE = "SelectedTitle";
|
|
||||||
public static final int MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0;
|
|
||||||
public static final int MENU_ACTION_TOGGLE_CONTROLS = 1;
|
|
||||||
public static final int MENU_ACTION_ADJUST_SCALE = 2;
|
|
||||||
public static final int MENU_ACTION_EXIT = 3;
|
|
||||||
public static final int MENU_ACTION_SHOW_FPS = 4;
|
|
||||||
public static final int MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE = 5;
|
|
||||||
public static final int MENU_ACTION_SCREEN_LAYOUT_PORTRAIT = 6;
|
|
||||||
public static final int MENU_ACTION_SCREEN_LAYOUT_SINGLE = 7;
|
|
||||||
public static final int MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE = 8;
|
|
||||||
public static final int MENU_ACTION_SWAP_SCREENS = 9;
|
|
||||||
public static final int MENU_ACTION_RESET_OVERLAY = 10;
|
|
||||||
public static final int MENU_ACTION_SHOW_OVERLAY = 11;
|
|
||||||
public static final int MENU_ACTION_OPEN_SETTINGS = 12;
|
|
||||||
public static final int MENU_ACTION_LOAD_AMIIBO = 13;
|
|
||||||
public static final int MENU_ACTION_REMOVE_AMIIBO = 14;
|
|
||||||
public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 15;
|
|
||||||
public static final int MENU_ACTION_DPAD_SLIDE_ENABLE = 16;
|
|
||||||
public static final int MENU_ACTION_OPEN_CHEATS = 17;
|
|
||||||
public static final int MENU_ACTION_CLOSE_GAME = 18;
|
|
||||||
|
|
||||||
public static final int REQUEST_SELECT_AMIIBO = 2;
|
|
||||||
private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
|
|
||||||
private static SparseIntArray buttonsActionsMap = new SparseIntArray();
|
|
||||||
|
|
||||||
private final ActivityResultLauncher<Boolean> mOpenFileLauncher =
|
|
||||||
registerForActivityResult(new OpenFileResultContract(), result -> {
|
|
||||||
if (result == null)
|
|
||||||
return;
|
|
||||||
String[] selectedFiles = FileBrowserHelper.getSelectedFiles(
|
|
||||||
result, getApplicationContext(), Collections.singletonList("bin"));
|
|
||||||
if (selectedFiles == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
onAmiiboSelected(selectedFiles[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
static {
|
|
||||||
buttonsActionsMap.append(R.id.menu_emulation_edit_layout,
|
|
||||||
EmulationActivity.MENU_ACTION_EDIT_CONTROLS_PLACEMENT);
|
|
||||||
buttonsActionsMap.append(R.id.menu_emulation_toggle_controls,
|
|
||||||
EmulationActivity.MENU_ACTION_TOGGLE_CONTROLS);
|
|
||||||
buttonsActionsMap
|
|
||||||
.append(R.id.menu_emulation_adjust_scale, EmulationActivity.MENU_ACTION_ADJUST_SCALE);
|
|
||||||
buttonsActionsMap.append(R.id.menu_emulation_show_fps,
|
|
||||||
EmulationActivity.MENU_ACTION_SHOW_FPS);
|
|
||||||
buttonsActionsMap.append(R.id.menu_screen_layout_landscape,
|
|
||||||
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE);
|
|
||||||
buttonsActionsMap.append(R.id.menu_screen_layout_portrait,
|
|
||||||
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_PORTRAIT);
|
|
||||||
buttonsActionsMap.append(R.id.menu_screen_layout_single,
|
|
||||||
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SINGLE);
|
|
||||||
buttonsActionsMap.append(R.id.menu_screen_layout_sidebyside,
|
|
||||||
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE);
|
|
||||||
buttonsActionsMap.append(R.id.menu_emulation_swap_screens,
|
|
||||||
EmulationActivity.MENU_ACTION_SWAP_SCREENS);
|
|
||||||
buttonsActionsMap
|
|
||||||
.append(R.id.menu_emulation_reset_overlay, EmulationActivity.MENU_ACTION_RESET_OVERLAY);
|
|
||||||
buttonsActionsMap
|
|
||||||
.append(R.id.menu_emulation_show_overlay, EmulationActivity.MENU_ACTION_SHOW_OVERLAY);
|
|
||||||
buttonsActionsMap
|
|
||||||
.append(R.id.menu_emulation_open_settings, EmulationActivity.MENU_ACTION_OPEN_SETTINGS);
|
|
||||||
buttonsActionsMap
|
|
||||||
.append(R.id.menu_emulation_amiibo_load, EmulationActivity.MENU_ACTION_LOAD_AMIIBO);
|
|
||||||
buttonsActionsMap
|
|
||||||
.append(R.id.menu_emulation_amiibo_remove, EmulationActivity.MENU_ACTION_REMOVE_AMIIBO);
|
|
||||||
buttonsActionsMap.append(R.id.menu_emulation_joystick_rel_center,
|
|
||||||
EmulationActivity.MENU_ACTION_JOYSTICK_REL_CENTER);
|
|
||||||
buttonsActionsMap.append(R.id.menu_emulation_dpad_slide_enable,
|
|
||||||
EmulationActivity.MENU_ACTION_DPAD_SLIDE_ENABLE);
|
|
||||||
buttonsActionsMap
|
|
||||||
.append(R.id.menu_emulation_open_cheats, EmulationActivity.MENU_ACTION_OPEN_CHEATS);
|
|
||||||
buttonsActionsMap
|
|
||||||
.append(R.id.menu_emulation_close_game, EmulationActivity.MENU_ACTION_CLOSE_GAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
private EmulationFragment mEmulationFragment;
|
|
||||||
private SharedPreferences mPreferences;
|
|
||||||
private ControllerMappingHelper mControllerMappingHelper;
|
|
||||||
private Intent foregroundService;
|
|
||||||
private boolean activityRecreated;
|
|
||||||
private String mSelectedTitle;
|
|
||||||
private String mPath;
|
|
||||||
|
|
||||||
public static void launch(FragmentActivity activity, String path, String title) {
|
|
||||||
Intent launcher = new Intent(activity, EmulationActivity.class);
|
|
||||||
|
|
||||||
launcher.putExtra(EXTRA_SELECTED_GAME, path);
|
|
||||||
launcher.putExtra(EXTRA_SELECTED_TITLE, title);
|
|
||||||
activity.startActivity(launcher);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void tryDismissRunningNotification(Activity activity) {
|
|
||||||
NotificationManagerCompat.from(activity).cancel(EMULATION_RUNNING_NOTIFICATION);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
stopService(foregroundService);
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
Log.gameLaunched = true;
|
|
||||||
ThemeUtil.INSTANCE.setTheme(this);
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
// Get params we were passed
|
|
||||||
Intent gameToEmulate = getIntent();
|
|
||||||
mPath = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME);
|
|
||||||
mSelectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE);
|
|
||||||
activityRecreated = false;
|
|
||||||
} else {
|
|
||||||
activityRecreated = true;
|
|
||||||
restoreState(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
mControllerMappingHelper = new ControllerMappingHelper();
|
|
||||||
|
|
||||||
// Set these options now so that the SurfaceView the game renders into is the right size.
|
|
||||||
enableFullscreenImmersive();
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_emulation);
|
|
||||||
|
|
||||||
// Find or create the EmulationFragment
|
|
||||||
mEmulationFragment = (EmulationFragment) getSupportFragmentManager()
|
|
||||||
.findFragmentById(R.id.frame_emulation_fragment);
|
|
||||||
if (mEmulationFragment == null) {
|
|
||||||
mEmulationFragment = EmulationFragment.newInstance(mPath);
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
|
||||||
.add(R.id.frame_emulation_fragment, mEmulationFragment)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTitle(mSelectedTitle);
|
|
||||||
|
|
||||||
mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
|
|
||||||
// Start a foreground service to prevent the app from getting killed in the background
|
|
||||||
foregroundService = new Intent(EmulationActivity.this, ForegroundService.class);
|
|
||||||
startForegroundService(foregroundService);
|
|
||||||
|
|
||||||
// Override Citra core INI with the one set by our in game menu
|
|
||||||
NativeLibrary.INSTANCE.swapScreens(EmulationMenuSettings.getSwapScreens(),
|
|
||||||
getWindowManager().getDefaultDisplay().getRotation());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
|
||||||
outState.putString(EXTRA_SELECTED_GAME, mPath);
|
|
||||||
outState.putString(EXTRA_SELECTED_TITLE, mSelectedTitle);
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void restoreState(Bundle savedInstanceState) {
|
|
||||||
mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME);
|
|
||||||
mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRestart() {
|
|
||||||
super.onRestart();
|
|
||||||
NativeLibrary.INSTANCE.reloadCameraDevices();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
View anchor = findViewById(R.id.menu_anchor);
|
|
||||||
PopupMenu popupMenu = new PopupMenu(this, anchor);
|
|
||||||
onCreateOptionsMenu(popupMenu.getMenu(), popupMenu.getMenuInflater());
|
|
||||||
updateSavestateMenuOptions(popupMenu.getMenu());
|
|
||||||
popupMenu.setOnMenuItemClickListener(this::onOptionsItemSelected);
|
|
||||||
popupMenu.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
||||||
switch (requestCode) {
|
|
||||||
case NativeLibrary.REQUEST_CODE_NATIVE_CAMERA:
|
|
||||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
|
||||||
shouldShowRequestPermissionRationale(CAMERA)) {
|
|
||||||
new MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(R.string.camera)
|
|
||||||
.setMessage(R.string.camera_permission_needed)
|
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
NativeLibrary.INSTANCE.cameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
|
||||||
break;
|
|
||||||
case NativeLibrary.REQUEST_CODE_NATIVE_MIC:
|
|
||||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
|
||||||
shouldShowRequestPermissionRationale(RECORD_AUDIO)) {
|
|
||||||
new MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(R.string.microphone)
|
|
||||||
.setMessage(R.string.microphone_permission_needed)
|
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
NativeLibrary.INSTANCE.micPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onEmulationStarted() {
|
|
||||||
Toast.makeText(this, getString(R.string.emulation_menu_help), Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void enableFullscreenImmersive() {
|
|
||||||
// TODO: Remove this once we properly account for display insets in the input overlay
|
|
||||||
getWindow().getAttributes().layoutInDisplayCutoutMode =
|
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
|
|
||||||
|
|
||||||
getWindow().getDecorView().setSystemUiVisibility(
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
|
||||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
|
||||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
|
||||||
View.SYSTEM_UI_FLAG_IMMERSIVE |
|
|
||||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
// Inflate the menu; this adds items to the action bar if it is present.
|
|
||||||
onCreateOptionsMenu(menu, getMenuInflater());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
|
||||||
inflater.inflate(R.menu.menu_emulation, menu);
|
|
||||||
|
|
||||||
int layoutOptionMenuItem = R.id.menu_screen_layout_landscape;
|
|
||||||
switch (EmulationMenuSettings.getLandscapeScreenLayout()) {
|
|
||||||
case EmulationMenuSettings.LayoutOption_SingleScreen:
|
|
||||||
layoutOptionMenuItem = R.id.menu_screen_layout_single;
|
|
||||||
break;
|
|
||||||
case EmulationMenuSettings.LayoutOption_SideScreen:
|
|
||||||
layoutOptionMenuItem = R.id.menu_screen_layout_sidebyside;
|
|
||||||
break;
|
|
||||||
case EmulationMenuSettings.LayoutOption_MobilePortrait:
|
|
||||||
layoutOptionMenuItem = R.id.menu_screen_layout_portrait;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.findItem(layoutOptionMenuItem).setChecked(true);
|
|
||||||
menu.findItem(R.id.menu_emulation_joystick_rel_center).setChecked(EmulationMenuSettings.getJoystickRelCenter());
|
|
||||||
menu.findItem(R.id.menu_emulation_dpad_slide_enable).setChecked(EmulationMenuSettings.getDpadSlideEnable());
|
|
||||||
menu.findItem(R.id.menu_emulation_show_fps).setChecked(EmulationMenuSettings.getShowFps());
|
|
||||||
menu.findItem(R.id.menu_emulation_swap_screens).setChecked(EmulationMenuSettings.getSwapScreens());
|
|
||||||
menu.findItem(R.id.menu_emulation_show_overlay).setChecked(EmulationMenuSettings.getShowOverlay());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DisplaySavestateWarning() {
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
|
|
||||||
if (preferences.getBoolean("savestateWarningShown", false)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LayoutInflater inflater = mEmulationFragment.requireActivity().getLayoutInflater();
|
|
||||||
View view = inflater.inflate(R.layout.dialog_checkbox, null);
|
|
||||||
CheckBox checkBox = view.findViewById(R.id.checkBox);
|
|
||||||
|
|
||||||
new MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(R.string.savestate_warning_title)
|
|
||||||
.setMessage(R.string.savestate_warning_message)
|
|
||||||
.setView(view)
|
|
||||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
|
||||||
preferences.edit().putBoolean("savestateWarningShown", checkBox.isChecked()).apply();
|
|
||||||
})
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
|
||||||
super.onPrepareOptionsMenu(menu);
|
|
||||||
updateSavestateMenuOptions(menu);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateSavestateMenuOptions(Menu menu) {
|
|
||||||
final NativeLibrary.SaveStateInfo[] savestates = NativeLibrary.INSTANCE.getSavestateInfo();
|
|
||||||
if (savestates == null) {
|
|
||||||
menu.findItem(R.id.menu_emulation_save_state).setVisible(false);
|
|
||||||
menu.findItem(R.id.menu_emulation_load_state).setVisible(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
menu.findItem(R.id.menu_emulation_save_state).setVisible(true);
|
|
||||||
menu.findItem(R.id.menu_emulation_load_state).setVisible(true);
|
|
||||||
|
|
||||||
final SubMenu saveStateMenu = menu.findItem(R.id.menu_emulation_save_state).getSubMenu();
|
|
||||||
final SubMenu loadStateMenu = menu.findItem(R.id.menu_emulation_load_state).getSubMenu();
|
|
||||||
saveStateMenu.clear();
|
|
||||||
loadStateMenu.clear();
|
|
||||||
|
|
||||||
// Update savestates information
|
|
||||||
for (int i = 0; i < NativeLibrary.SAVESTATE_SLOT_COUNT; ++i) {
|
|
||||||
final int slot = i + 1;
|
|
||||||
final String text = getString(R.string.emulation_empty_state_slot, slot);
|
|
||||||
saveStateMenu.add(text).setEnabled(true).setOnMenuItemClickListener((item) -> {
|
|
||||||
DisplaySavestateWarning();
|
|
||||||
NativeLibrary.INSTANCE.saveState(slot);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
loadStateMenu.add(text).setEnabled(false).setOnMenuItemClickListener((item) -> {
|
|
||||||
NativeLibrary.INSTANCE.loadState(slot);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (final NativeLibrary.SaveStateInfo info : savestates) {
|
|
||||||
final String text = getString(R.string.emulation_occupied_state_slot, info.getSlot(), info.getTime());
|
|
||||||
saveStateMenu.getItem(info.getSlot() - 1).setTitle(text);
|
|
||||||
loadStateMenu.getItem(info.getSlot() - 1).setTitle(text).setEnabled(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("WrongConstant")
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
int action = buttonsActionsMap.get(item.getItemId(), -1);
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
// Edit the placement of the controls
|
|
||||||
case MENU_ACTION_EDIT_CONTROLS_PLACEMENT:
|
|
||||||
editControlsPlacement();
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Enable/Disable specific buttons or the entire input overlay.
|
|
||||||
case MENU_ACTION_TOGGLE_CONTROLS:
|
|
||||||
toggleControls();
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Adjust the scale of the overlay controls.
|
|
||||||
case MENU_ACTION_ADJUST_SCALE:
|
|
||||||
adjustScale();
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Toggle the visibility of the Performance stats TextView
|
|
||||||
case MENU_ACTION_SHOW_FPS: {
|
|
||||||
final boolean isEnabled = !EmulationMenuSettings.getShowFps();
|
|
||||||
EmulationMenuSettings.setShowFps(isEnabled);
|
|
||||||
item.setChecked(isEnabled);
|
|
||||||
|
|
||||||
mEmulationFragment.updateShowFpsOverlay();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Sets the screen layout to Landscape
|
|
||||||
case MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE:
|
|
||||||
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobileLandscape, item);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Sets the screen layout to Portrait
|
|
||||||
case MENU_ACTION_SCREEN_LAYOUT_PORTRAIT:
|
|
||||||
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobilePortrait, item);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Sets the screen layout to Single
|
|
||||||
case MENU_ACTION_SCREEN_LAYOUT_SINGLE:
|
|
||||||
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SingleScreen, item);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Sets the screen layout to Side by Side
|
|
||||||
case MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE:
|
|
||||||
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SideScreen, item);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Swap the top and bottom screen locations
|
|
||||||
case MENU_ACTION_SWAP_SCREENS: {
|
|
||||||
final boolean isEnabled = !EmulationMenuSettings.getSwapScreens();
|
|
||||||
EmulationMenuSettings.setSwapScreens(isEnabled);
|
|
||||||
item.setChecked(isEnabled);
|
|
||||||
|
|
||||||
NativeLibrary.INSTANCE.swapScreens(isEnabled, getWindowManager().getDefaultDisplay()
|
|
||||||
.getRotation());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset overlay placement
|
|
||||||
case MENU_ACTION_RESET_OVERLAY:
|
|
||||||
resetOverlay();
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Show or hide overlay
|
|
||||||
case MENU_ACTION_SHOW_OVERLAY: {
|
|
||||||
final boolean isEnabled = !EmulationMenuSettings.getShowOverlay();
|
|
||||||
EmulationMenuSettings.setShowOverlay(isEnabled);
|
|
||||||
item.setChecked(isEnabled);
|
|
||||||
|
|
||||||
mEmulationFragment.refreshInputOverlay();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case MENU_ACTION_EXIT:
|
|
||||||
mEmulationFragment.stopEmulation();
|
|
||||||
finish();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MENU_ACTION_OPEN_SETTINGS:
|
|
||||||
SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, "");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MENU_ACTION_LOAD_AMIIBO:
|
|
||||||
mOpenFileLauncher.launch(false);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MENU_ACTION_REMOVE_AMIIBO:
|
|
||||||
RemoveAmiibo();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MENU_ACTION_JOYSTICK_REL_CENTER:
|
|
||||||
final boolean isJoystickRelCenterEnabled = !EmulationMenuSettings.getJoystickRelCenter();
|
|
||||||
EmulationMenuSettings.setJoystickRelCenter(isJoystickRelCenterEnabled);
|
|
||||||
item.setChecked(isJoystickRelCenterEnabled);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MENU_ACTION_DPAD_SLIDE_ENABLE:
|
|
||||||
final boolean isDpadSlideEnabled = !EmulationMenuSettings.getDpadSlideEnable();
|
|
||||||
EmulationMenuSettings.setDpadSlideEnable(isDpadSlideEnabled);
|
|
||||||
item.setChecked(isDpadSlideEnabled);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MENU_ACTION_OPEN_CHEATS:
|
|
||||||
CheatsActivity.launch(this, NativeLibrary.INSTANCE.getRunningTitleId());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MENU_ACTION_CLOSE_GAME:
|
|
||||||
NativeLibrary.INSTANCE.pauseEmulation();
|
|
||||||
new MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(R.string.emulation_close_game)
|
|
||||||
.setMessage(R.string.emulation_close_game_message)
|
|
||||||
.setPositiveButton(android.R.string.yes, (dialogInterface, i) ->
|
|
||||||
{
|
|
||||||
mEmulationFragment.stopEmulation();
|
|
||||||
finish();
|
|
||||||
})
|
|
||||||
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> NativeLibrary.INSTANCE.unPauseEmulation())
|
|
||||||
.setOnCancelListener(dialogInterface -> NativeLibrary.INSTANCE.unPauseEmulation())
|
|
||||||
.show();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void changeScreenOrientation(int layoutOption, MenuItem item) {
|
|
||||||
item.setChecked(true);
|
|
||||||
NativeLibrary.INSTANCE.notifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay()
|
|
||||||
.getRotation());
|
|
||||||
EmulationMenuSettings.setLandscapeScreenLayout(layoutOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void editControlsPlacement() {
|
|
||||||
if (mEmulationFragment.isConfiguringControls()) {
|
|
||||||
mEmulationFragment.stopConfiguringControls();
|
|
||||||
} else {
|
|
||||||
mEmulationFragment.startConfiguringControls();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets button presses
|
|
||||||
@Override
|
|
||||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
|
||||||
int action;
|
|
||||||
int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode());
|
|
||||||
|
|
||||||
switch (event.getAction()) {
|
|
||||||
case KeyEvent.ACTION_DOWN:
|
|
||||||
// Handling the case where the back button is pressed.
|
|
||||||
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
|
||||||
onBackPressed();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal key events.
|
|
||||||
action = NativeLibrary.ButtonState.PRESSED;
|
|
||||||
break;
|
|
||||||
case KeyEvent.ACTION_UP:
|
|
||||||
action = NativeLibrary.ButtonState.RELEASED;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
InputDevice input = event.getDevice();
|
|
||||||
|
|
||||||
if (input == null) {
|
|
||||||
// Controller was disconnected
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NativeLibrary.INSTANCE.onGamePadEvent(input.getDescriptor(), button, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, result);
|
|
||||||
if (requestCode == StillImageCameraHelper.REQUEST_CAMERA_FILE_PICKER) {
|
|
||||||
StillImageCameraHelper.OnFilePickerResult(resultCode == RESULT_OK ? result : null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onAmiiboSelected(String selectedFile) {
|
|
||||||
boolean success = NativeLibrary.INSTANCE.loadAmiibo(selectedFile);
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
new MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(R.string.amiibo_load_error)
|
|
||||||
.setMessage(R.string.amiibo_load_error_message)
|
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveAmiibo() {
|
|
||||||
NativeLibrary.INSTANCE.removeAmiibo();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toggleControls() {
|
|
||||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
|
||||||
boolean[] enabledButtons = new boolean[14];
|
|
||||||
|
|
||||||
for (int i = 0; i < enabledButtons.length; i++) {
|
|
||||||
// Buttons that are disabled by default
|
|
||||||
boolean defaultValue = true;
|
|
||||||
switch (i) {
|
|
||||||
case 6: // ZL
|
|
||||||
case 7: // ZR
|
|
||||||
case 12: // C-stick
|
|
||||||
defaultValue = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
enabledButtons[i] = mPreferences.getBoolean("buttonToggle" + i, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
new MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(R.string.emulation_toggle_controls)
|
|
||||||
.setMultiChoiceItems(R.array.n3dsButtons, enabledButtons,
|
|
||||||
(dialog, indexSelected, isChecked) -> editor
|
|
||||||
.putBoolean("buttonToggle" + indexSelected, isChecked))
|
|
||||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
|
|
||||||
{
|
|
||||||
editor.apply();
|
|
||||||
mEmulationFragment.refreshInputOverlay();
|
|
||||||
})
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void adjustScale() {
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(this);
|
|
||||||
View view = inflater.inflate(R.layout.dialog_slider, null);
|
|
||||||
|
|
||||||
final Slider slider = view.findViewById(R.id.slider);
|
|
||||||
final TextView textValue = view.findViewById(R.id.text_value);
|
|
||||||
final TextView units = view.findViewById(R.id.text_units);
|
|
||||||
|
|
||||||
slider.setValueTo(150);
|
|
||||||
slider.setValue(mPreferences.getInt("controlScale", 50));
|
|
||||||
slider.addOnChangeListener((slider1, progress, fromUser) -> {
|
|
||||||
textValue.setText(String.valueOf((int) progress + 50));
|
|
||||||
setControlScale((int) slider1.getValue());
|
|
||||||
});
|
|
||||||
|
|
||||||
textValue.setText(String.valueOf((int) slider.getValue() + 50));
|
|
||||||
units.setText("%");
|
|
||||||
|
|
||||||
final int previousProgress = (int) slider.getValue();
|
|
||||||
|
|
||||||
new MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(R.string.emulation_control_scale)
|
|
||||||
.setView(view)
|
|
||||||
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> setControlScale(previousProgress))
|
|
||||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> setControlScale((int) slider.getValue()))
|
|
||||||
.setNeutralButton(R.string.slider_default, (dialogInterface, i) -> setControlScale(50))
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setControlScale(int scale) {
|
|
||||||
SharedPreferences.Editor editor = mPreferences.edit();
|
|
||||||
editor.putInt("controlScale", scale);
|
|
||||||
editor.apply();
|
|
||||||
mEmulationFragment.refreshInputOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetOverlay() {
|
|
||||||
new MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(getString(R.string.emulation_touch_overlay_reset))
|
|
||||||
.setPositiveButton(android.R.string.yes, (dialogInterface, i) -> mEmulationFragment.resetInputOverlay())
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean dispatchGenericMotionEvent(MotionEvent event) {
|
|
||||||
if (((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)) {
|
|
||||||
return super.dispatchGenericMotionEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't attempt to do anything if we are disconnecting a device.
|
|
||||||
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
InputDevice input = event.getDevice();
|
|
||||||
List<InputDevice.MotionRange> motions = input.getMotionRanges();
|
|
||||||
|
|
||||||
float[] axisValuesCirclePad = {0.0f, 0.0f};
|
|
||||||
float[] axisValuesCStick = {0.0f, 0.0f};
|
|
||||||
float[] axisValuesDPad = {0.0f, 0.0f};
|
|
||||||
boolean isTriggerPressedLMapped = false;
|
|
||||||
boolean isTriggerPressedRMapped = false;
|
|
||||||
boolean isTriggerPressedZLMapped = false;
|
|
||||||
boolean isTriggerPressedZRMapped = false;
|
|
||||||
boolean isTriggerPressedL = false;
|
|
||||||
boolean isTriggerPressedR = false;
|
|
||||||
boolean isTriggerPressedZL = false;
|
|
||||||
boolean isTriggerPressedZR = false;
|
|
||||||
|
|
||||||
for (InputDevice.MotionRange range : motions) {
|
|
||||||
int axis = range.getAxis();
|
|
||||||
float origValue = event.getAxisValue(axis);
|
|
||||||
float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
|
|
||||||
int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1);
|
|
||||||
int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1);
|
|
||||||
|
|
||||||
if (nextMapping == -1 || guestOrientation == -1) {
|
|
||||||
// Axis is unmapped
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((value > 0.f && value < 0.1f) || (value < 0.f && value > -0.1f)) {
|
|
||||||
// Skip joystick wobble
|
|
||||||
value = 0.f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextMapping == NativeLibrary.ButtonType.STICK_LEFT) {
|
|
||||||
axisValuesCirclePad[guestOrientation] = value;
|
|
||||||
} else if (nextMapping == NativeLibrary.ButtonType.STICK_C) {
|
|
||||||
axisValuesCStick[guestOrientation] = value;
|
|
||||||
} else if (nextMapping == NativeLibrary.ButtonType.DPAD) {
|
|
||||||
axisValuesDPad[guestOrientation] = value;
|
|
||||||
} else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_L) {
|
|
||||||
isTriggerPressedLMapped = true;
|
|
||||||
isTriggerPressedL = value != 0.f;
|
|
||||||
} else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_R) {
|
|
||||||
isTriggerPressedRMapped = true;
|
|
||||||
isTriggerPressedR = value != 0.f;
|
|
||||||
} else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZL) {
|
|
||||||
isTriggerPressedZLMapped = true;
|
|
||||||
isTriggerPressedZL = value != 0.f;
|
|
||||||
} else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZR) {
|
|
||||||
isTriggerPressedZRMapped = true;
|
|
||||||
isTriggerPressedZR = value != 0.f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Circle-Pad and C-Stick status
|
|
||||||
NativeLibrary.INSTANCE.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]);
|
|
||||||
NativeLibrary.INSTANCE.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]);
|
|
||||||
|
|
||||||
// Triggers L/R and ZL/ZR
|
|
||||||
if (isTriggerPressedLMapped) {
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
|
||||||
}
|
|
||||||
if (isTriggerPressedRMapped) {
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
|
||||||
}
|
|
||||||
if (isTriggerPressedZLMapped) {
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
|
||||||
}
|
|
||||||
if (isTriggerPressedZRMapped) {
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Work-around to allow D-pad axis to be bound to emulated buttons
|
|
||||||
if (axisValuesDPad[0] == 0.f) {
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
|
||||||
}
|
|
||||||
if (axisValuesDPad[0] < 0.f) {
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED);
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
|
||||||
}
|
|
||||||
if (axisValuesDPad[0] > 0.f) {
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED);
|
|
||||||
}
|
|
||||||
if (axisValuesDPad[1] == 0.f) {
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
|
||||||
}
|
|
||||||
if (axisValuesDPad[1] < 0.f) {
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED);
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
|
||||||
}
|
|
||||||
if (axisValuesDPad[1] > 0.f) {
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
|
||||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isActivityRecreated() {
|
|
||||||
return activityRecreated;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Retention(SOURCE)
|
|
||||||
@IntDef({MENU_ACTION_EDIT_CONTROLS_PLACEMENT, MENU_ACTION_TOGGLE_CONTROLS, MENU_ACTION_ADJUST_SCALE,
|
|
||||||
MENU_ACTION_EXIT, MENU_ACTION_SHOW_FPS, MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE,
|
|
||||||
MENU_ACTION_SCREEN_LAYOUT_PORTRAIT, MENU_ACTION_SCREEN_LAYOUT_SINGLE, MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE,
|
|
||||||
MENU_ACTION_SWAP_SCREENS, MENU_ACTION_RESET_OVERLAY, MENU_ACTION_SHOW_OVERLAY, MENU_ACTION_OPEN_SETTINGS})
|
|
||||||
public @interface MenuAction {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,464 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.activities
|
||||||
|
|
||||||
|
import android.Manifest.permission
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.InputDevice
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import org.citra.citra_emu.CitraApplication
|
||||||
|
import org.citra.citra_emu.NativeLibrary
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult
|
||||||
|
import org.citra.citra_emu.contracts.OpenFileResultContract
|
||||||
|
import org.citra.citra_emu.databinding.ActivityEmulationBinding
|
||||||
|
import org.citra.citra_emu.display.ScreenAdjustmentUtil
|
||||||
|
import org.citra.citra_emu.features.hotkeys.HotkeyUtility
|
||||||
|
import org.citra.citra_emu.features.settings.model.SettingsViewModel
|
||||||
|
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
|
||||||
|
import org.citra.citra_emu.fragments.MessageDialogFragment
|
||||||
|
import org.citra.citra_emu.utils.ControllerMappingHelper
|
||||||
|
import org.citra.citra_emu.utils.FileBrowserHelper
|
||||||
|
import org.citra.citra_emu.utils.ForegroundService
|
||||||
|
import org.citra.citra_emu.utils.EmulationLifecycleUtil
|
||||||
|
import org.citra.citra_emu.utils.EmulationMenuSettings
|
||||||
|
import org.citra.citra_emu.utils.ThemeUtil
|
||||||
|
import org.citra.citra_emu.viewmodel.EmulationViewModel
|
||||||
|
|
||||||
|
class EmulationActivity : AppCompatActivity() {
|
||||||
|
private val preferences: SharedPreferences
|
||||||
|
get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||||
|
private var foregroundService: Intent? = null
|
||||||
|
var isActivityRecreated = false
|
||||||
|
|
||||||
|
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||||
|
private val emulationViewModel: EmulationViewModel by viewModels()
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityEmulationBinding
|
||||||
|
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
|
||||||
|
private lateinit var hotkeyUtility: HotkeyUtility
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
ThemeUtil.setTheme(this)
|
||||||
|
|
||||||
|
settingsViewModel.settings.loadSettings()
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
binding = ActivityEmulationBinding.inflate(layoutInflater)
|
||||||
|
screenAdjustmentUtil = ScreenAdjustmentUtil(windowManager, settingsViewModel.settings)
|
||||||
|
hotkeyUtility = HotkeyUtility(screenAdjustmentUtil)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
val navHostFragment =
|
||||||
|
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
||||||
|
val navController = navHostFragment.navController
|
||||||
|
navController.setGraph(R.navigation.emulation_navigation, intent.extras)
|
||||||
|
|
||||||
|
isActivityRecreated = savedInstanceState != null
|
||||||
|
|
||||||
|
// Set these options now so that the SurfaceView the game renders into is the right size.
|
||||||
|
enableFullscreenImmersive()
|
||||||
|
|
||||||
|
// Override Citra core INI with the one set by our in game menu
|
||||||
|
NativeLibrary.swapScreens(
|
||||||
|
EmulationMenuSettings.swapScreens,
|
||||||
|
windowManager.defaultDisplay.rotation
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start a foreground service to prevent the app from getting killed in the background
|
||||||
|
foregroundService = Intent(this, ForegroundService::class.java)
|
||||||
|
startForegroundService(foregroundService)
|
||||||
|
|
||||||
|
EmulationLifecycleUtil.addShutdownHook(hook = { this.finish() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// On some devices, the system bars will not disappear on first boot or after some
|
||||||
|
// rotations. Here we set full screen immersive repeatedly in onResume and in
|
||||||
|
// onWindowFocusChanged to prevent the unwanted status bar state.
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
enableFullscreenImmersive()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
|
super.onWindowFocusChanged(hasFocus)
|
||||||
|
enableFullscreenImmersive()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onRestart() {
|
||||||
|
super.onRestart()
|
||||||
|
NativeLibrary.reloadCameraDevices()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
EmulationLifecycleUtil.clear()
|
||||||
|
stopForegroundService(this)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(
|
||||||
|
requestCode: Int,
|
||||||
|
permissions: Array<String>,
|
||||||
|
grantResults: IntArray
|
||||||
|
) {
|
||||||
|
when (requestCode) {
|
||||||
|
NativeLibrary.REQUEST_CODE_NATIVE_CAMERA -> {
|
||||||
|
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
||||||
|
shouldShowRequestPermissionRationale(permission.CAMERA)
|
||||||
|
) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
R.string.camera,
|
||||||
|
R.string.camera_permission_needed
|
||||||
|
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
NativeLibrary.cameraPermissionResult(
|
||||||
|
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.REQUEST_CODE_NATIVE_MIC -> {
|
||||||
|
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
||||||
|
shouldShowRequestPermissionRationale(permission.RECORD_AUDIO)
|
||||||
|
) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
R.string.microphone,
|
||||||
|
R.string.microphone_permission_needed
|
||||||
|
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
NativeLibrary.micPermissionResult(
|
||||||
|
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEmulationStarted() {
|
||||||
|
emulationViewModel.setEmulationStarted(true)
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
getString(R.string.emulation_menu_help),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enableFullscreenImmersive() {
|
||||||
|
// TODO: Remove this once we properly account for display insets in the input overlay
|
||||||
|
window.attributes.layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
|
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
|
||||||
|
controller.hide(WindowInsetsCompat.Type.systemBars())
|
||||||
|
controller.systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets button presses
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@SuppressLint("GestureBackNavigation")
|
||||||
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
|
// TODO: Move this check into native code - prevents crash if input pressed before starting emulation
|
||||||
|
if (!NativeLibrary.isRunning()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val button =
|
||||||
|
preferences.getInt(InputBindingSetting.getInputButtonKey(event.keyCode), event.keyCode)
|
||||||
|
val action: Int = when (event.action) {
|
||||||
|
KeyEvent.ACTION_DOWN -> {
|
||||||
|
// On some devices, the back gesture / button press is not intercepted by androidx
|
||||||
|
// and fails to open the emulation menu. So we're stuck running deprecated code to
|
||||||
|
// cover for either a fault on androidx's side or in OEM skins (MIUI at least)
|
||||||
|
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
hotkeyUtility.handleHotkey(button)
|
||||||
|
|
||||||
|
// Normal key events.
|
||||||
|
NativeLibrary.ButtonState.PRESSED
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
val input = event.device
|
||||||
|
?: // Controller was disconnected
|
||||||
|
return false
|
||||||
|
return NativeLibrary.onGamePadEvent(input.descriptor, button, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAmiiboSelected(selectedFile: String) {
|
||||||
|
val success = NativeLibrary.loadAmiibo(selectedFile)
|
||||||
|
if (!success) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
R.string.amiibo_load_error,
|
||||||
|
R.string.amiibo_load_error_message
|
||||||
|
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
|
||||||
|
// TODO: Move this check into native code - prevents crash if input pressed before starting emulation
|
||||||
|
if (!NativeLibrary.isRunning()) {
|
||||||
|
return super.dispatchGenericMotionEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.source and InputDevice.SOURCE_CLASS_JOYSTICK == 0) {
|
||||||
|
return super.dispatchGenericMotionEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't attempt to do anything if we are disconnecting a device.
|
||||||
|
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val input = event.device
|
||||||
|
val motions = input.motionRanges
|
||||||
|
val axisValuesCirclePad = floatArrayOf(0.0f, 0.0f)
|
||||||
|
val axisValuesCStick = floatArrayOf(0.0f, 0.0f)
|
||||||
|
val axisValuesDPad = floatArrayOf(0.0f, 0.0f)
|
||||||
|
var isTriggerPressedLMapped = false
|
||||||
|
var isTriggerPressedRMapped = false
|
||||||
|
var isTriggerPressedZLMapped = false
|
||||||
|
var isTriggerPressedZRMapped = false
|
||||||
|
var isTriggerPressedL = false
|
||||||
|
var isTriggerPressedR = false
|
||||||
|
var isTriggerPressedZL = false
|
||||||
|
var isTriggerPressedZR = false
|
||||||
|
for (range in motions) {
|
||||||
|
val axis = range.axis
|
||||||
|
val origValue = event.getAxisValue(axis)
|
||||||
|
var value = ControllerMappingHelper.scaleAxis(input, axis, origValue)
|
||||||
|
val nextMapping =
|
||||||
|
preferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1)
|
||||||
|
val guestOrientation =
|
||||||
|
preferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1)
|
||||||
|
if (nextMapping == -1 || guestOrientation == -1) {
|
||||||
|
// Axis is unmapped
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (value > 0f && value < 0.1f || value < 0f && value > -0.1f) {
|
||||||
|
// Skip joystick wobble
|
||||||
|
value = 0f
|
||||||
|
}
|
||||||
|
when (nextMapping) {
|
||||||
|
NativeLibrary.ButtonType.STICK_LEFT -> {
|
||||||
|
axisValuesCirclePad[guestOrientation] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.ButtonType.STICK_C -> {
|
||||||
|
axisValuesCStick[guestOrientation] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.ButtonType.DPAD -> {
|
||||||
|
axisValuesDPad[guestOrientation] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.ButtonType.TRIGGER_L -> {
|
||||||
|
isTriggerPressedLMapped = true
|
||||||
|
isTriggerPressedL = value != 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.ButtonType.TRIGGER_R -> {
|
||||||
|
isTriggerPressedRMapped = true
|
||||||
|
isTriggerPressedR = value != 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.ButtonType.BUTTON_ZL -> {
|
||||||
|
isTriggerPressedZLMapped = true
|
||||||
|
isTriggerPressedZL = value != 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.ButtonType.BUTTON_ZR -> {
|
||||||
|
isTriggerPressedZRMapped = true
|
||||||
|
isTriggerPressedZR = value != 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Circle-Pad and C-Stick status
|
||||||
|
NativeLibrary.onGamePadMoveEvent(
|
||||||
|
input.descriptor,
|
||||||
|
NativeLibrary.ButtonType.STICK_LEFT,
|
||||||
|
axisValuesCirclePad[0],
|
||||||
|
axisValuesCirclePad[1]
|
||||||
|
)
|
||||||
|
NativeLibrary.onGamePadMoveEvent(
|
||||||
|
input.descriptor,
|
||||||
|
NativeLibrary.ButtonType.STICK_C,
|
||||||
|
axisValuesCStick[0],
|
||||||
|
axisValuesCStick[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Triggers L/R and ZL/ZR
|
||||||
|
if (isTriggerPressedLMapped) {
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.TRIGGER_L,
|
||||||
|
if (isTriggerPressedL) {
|
||||||
|
NativeLibrary.ButtonState.PRESSED
|
||||||
|
} else {
|
||||||
|
NativeLibrary.ButtonState.RELEASED
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (isTriggerPressedRMapped) {
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.TRIGGER_R,
|
||||||
|
if (isTriggerPressedR) {
|
||||||
|
NativeLibrary.ButtonState.PRESSED
|
||||||
|
} else {
|
||||||
|
NativeLibrary.ButtonState.RELEASED
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (isTriggerPressedZLMapped) {
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.BUTTON_ZL,
|
||||||
|
if (isTriggerPressedZL) {
|
||||||
|
NativeLibrary.ButtonState.PRESSED
|
||||||
|
} else {
|
||||||
|
NativeLibrary.ButtonState.RELEASED
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (isTriggerPressedZRMapped) {
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.BUTTON_ZR,
|
||||||
|
if (isTriggerPressedZR) {
|
||||||
|
NativeLibrary.ButtonState.PRESSED
|
||||||
|
} else {
|
||||||
|
NativeLibrary.ButtonState.RELEASED
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Work-around to allow D-pad axis to be bound to emulated buttons
|
||||||
|
if (axisValuesDPad[0] == 0f) {
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.DPAD_LEFT,
|
||||||
|
NativeLibrary.ButtonState.RELEASED
|
||||||
|
)
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.DPAD_RIGHT,
|
||||||
|
NativeLibrary.ButtonState.RELEASED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (axisValuesDPad[0] < 0f) {
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.DPAD_LEFT,
|
||||||
|
NativeLibrary.ButtonState.PRESSED
|
||||||
|
)
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.DPAD_RIGHT,
|
||||||
|
NativeLibrary.ButtonState.RELEASED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (axisValuesDPad[0] > 0f) {
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.DPAD_LEFT,
|
||||||
|
NativeLibrary.ButtonState.RELEASED
|
||||||
|
)
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.DPAD_RIGHT,
|
||||||
|
NativeLibrary.ButtonState.PRESSED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (axisValuesDPad[1] == 0f) {
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.DPAD_UP,
|
||||||
|
NativeLibrary.ButtonState.RELEASED
|
||||||
|
)
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.DPAD_DOWN,
|
||||||
|
NativeLibrary.ButtonState.RELEASED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (axisValuesDPad[1] < 0f) {
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.DPAD_UP,
|
||||||
|
NativeLibrary.ButtonState.PRESSED
|
||||||
|
)
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.DPAD_DOWN,
|
||||||
|
NativeLibrary.ButtonState.RELEASED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (axisValuesDPad[1] > 0f) {
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.DPAD_UP,
|
||||||
|
NativeLibrary.ButtonState.RELEASED
|
||||||
|
)
|
||||||
|
NativeLibrary.onGamePadEvent(
|
||||||
|
NativeLibrary.TouchScreenDevice,
|
||||||
|
NativeLibrary.ButtonType.DPAD_DOWN,
|
||||||
|
NativeLibrary.ButtonState.PRESSED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
val openFileLauncher =
|
||||||
|
registerForActivityResult(OpenFileResultContract()) { result: Intent? ->
|
||||||
|
if (result == null) return@registerForActivityResult
|
||||||
|
val selectedFiles = FileBrowserHelper.getSelectedFiles(
|
||||||
|
result, applicationContext, listOf<String>("bin")
|
||||||
|
) ?: return@registerForActivityResult
|
||||||
|
onAmiiboSelected(selectedFiles[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
val openImageLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { result: Uri? ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
OnFilePickerResult(result.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun stopForegroundService(activity: Activity) {
|
||||||
|
val startIntent = Intent(activity, ForegroundService::class.java)
|
||||||
|
startIntent.action = ForegroundService.ACTION_STOP
|
||||||
|
activity.startForegroundService(startIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.navigation.findNavController
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
@ -22,12 +23,12 @@ import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.citra.citra_emu.HomeNavigationDirections
|
||||||
import org.citra.citra_emu.CitraApplication
|
import org.citra.citra_emu.CitraApplication
|
||||||
import org.citra.citra_emu.R
|
import org.citra.citra_emu.R
|
||||||
import org.citra.citra_emu.activities.EmulationActivity
|
|
||||||
import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder
|
import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder
|
||||||
import org.citra.citra_emu.databinding.CardGameBinding
|
import org.citra.citra_emu.databinding.CardGameBinding
|
||||||
import org.citra.citra_emu.features.cheats.ui.CheatsActivity
|
import org.citra.citra_emu.features.cheats.ui.CheatsFragmentDirections
|
||||||
import org.citra.citra_emu.model.Game
|
import org.citra.citra_emu.model.Game
|
||||||
import org.citra.citra_emu.utils.GameIconUtils
|
import org.citra.citra_emu.utils.GameIconUtils
|
||||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||||
|
@ -77,7 +78,8 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||||
)
|
)
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
EmulationActivity.launch(activity, holder.game.path, holder.game.title)
|
val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game)
|
||||||
|
view.findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -97,7 +99,8 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
CheatsActivity.launch(view.context, holder.game.titleId)
|
val action = CheatsFragmentDirections.actionGlobalCheatsFragment(holder.game.titleId)
|
||||||
|
view.findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,129 +0,0 @@
|
||||||
// Copyright 2020 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
package org.citra.citra_emu.applets;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.activities.EmulationActivity;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
public final class MiiSelector {
|
|
||||||
@Keep
|
|
||||||
public static class MiiSelectorConfig implements java.io.Serializable {
|
|
||||||
public boolean enable_cancel_button;
|
|
||||||
public String title;
|
|
||||||
public long initially_selected_mii_index;
|
|
||||||
// List of Miis to display
|
|
||||||
public String[] mii_names;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class MiiSelectorData {
|
|
||||||
public long return_code;
|
|
||||||
public int index;
|
|
||||||
|
|
||||||
private MiiSelectorData(long return_code, int index) {
|
|
||||||
this.return_code = return_code;
|
|
||||||
this.index = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class MiiSelectorDialogFragment extends DialogFragment {
|
|
||||||
static MiiSelectorDialogFragment newInstance(MiiSelectorConfig config) {
|
|
||||||
MiiSelectorDialogFragment frag = new MiiSelectorDialogFragment();
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putSerializable("config", config);
|
|
||||||
frag.setArguments(args);
|
|
||||||
return frag;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
final Activity emulationActivity = Objects.requireNonNull(getActivity());
|
|
||||||
|
|
||||||
MiiSelectorConfig config =
|
|
||||||
Objects.requireNonNull((MiiSelectorConfig) Objects.requireNonNull(getArguments())
|
|
||||||
.getSerializable("config"));
|
|
||||||
|
|
||||||
// Note: we intentionally leave out the Standard Mii in the native code so that
|
|
||||||
// the string can get translated
|
|
||||||
ArrayList<String> list = new ArrayList<>();
|
|
||||||
list.add(emulationActivity.getString(R.string.standard_mii));
|
|
||||||
list.addAll(Arrays.asList(config.mii_names));
|
|
||||||
|
|
||||||
final int initialIndex = config.initially_selected_mii_index < list.size()
|
|
||||||
? (int) config.initially_selected_mii_index
|
|
||||||
: 0;
|
|
||||||
data.index = initialIndex;
|
|
||||||
MaterialAlertDialogBuilder builder =
|
|
||||||
new MaterialAlertDialogBuilder(emulationActivity)
|
|
||||||
.setTitle(config.title.isEmpty()
|
|
||||||
? emulationActivity.getString(R.string.mii_selector)
|
|
||||||
: config.title)
|
|
||||||
.setSingleChoiceItems(list.toArray(new String[]{}), initialIndex,
|
|
||||||
(dialog, which) -> {
|
|
||||||
data.index = which;
|
|
||||||
})
|
|
||||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
|
||||||
data.return_code = 0;
|
|
||||||
synchronized (finishLock) {
|
|
||||||
finishLock.notifyAll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (config.enable_cancel_button) {
|
|
||||||
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
|
||||||
data.return_code = 1;
|
|
||||||
synchronized (finishLock) {
|
|
||||||
finishLock.notifyAll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setCancelable(false);
|
|
||||||
return builder.create();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MiiSelectorData data;
|
|
||||||
private static final Object finishLock = new Object();
|
|
||||||
|
|
||||||
private static void ExecuteImpl(MiiSelectorConfig config) {
|
|
||||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
|
||||||
|
|
||||||
data = new MiiSelectorData(0, 0);
|
|
||||||
|
|
||||||
MiiSelectorDialogFragment fragment = MiiSelectorDialogFragment.newInstance(config);
|
|
||||||
fragment.show(emulationActivity.getSupportFragmentManager(), "mii_selector");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MiiSelectorData Execute(MiiSelectorConfig config) {
|
|
||||||
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
|
|
||||||
|
|
||||||
synchronized (finishLock) {
|
|
||||||
try {
|
|
||||||
finishLock.wait();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.applets
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import org.citra.citra_emu.NativeLibrary
|
||||||
|
import org.citra.citra_emu.fragments.MiiSelectorDialogFragment
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
object MiiSelector {
|
||||||
|
lateinit var data: MiiSelectorData
|
||||||
|
val finishLock = Object()
|
||||||
|
|
||||||
|
private fun ExecuteImpl(config: MiiSelectorConfig) {
|
||||||
|
val emulationActivity = NativeLibrary.sEmulationActivity.get()
|
||||||
|
data = MiiSelectorData(0, 0)
|
||||||
|
val fragment = MiiSelectorDialogFragment.newInstance(config)
|
||||||
|
fragment.show(emulationActivity!!.supportFragmentManager, "mii_selector")
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun Execute(config: MiiSelectorConfig): MiiSelectorData {
|
||||||
|
NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { ExecuteImpl(config) }
|
||||||
|
synchronized(finishLock) {
|
||||||
|
try {
|
||||||
|
finishLock.wait()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
class MiiSelectorConfig : Serializable {
|
||||||
|
var enableCancelButton = false
|
||||||
|
var title: String? = null
|
||||||
|
var initiallySelectedMiiIndex: Long = 0
|
||||||
|
|
||||||
|
// List of Miis to display
|
||||||
|
lateinit var miiNames: Array<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
class MiiSelectorData (var returnCode: Long, var index: Int)
|
||||||
|
}
|
|
@ -1,279 +0,0 @@
|
||||||
// Copyright 2020 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
package org.citra.citra_emu.applets;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.InputFilter;
|
|
||||||
import android.text.Spanned;
|
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
|
||||||
import androidx.annotation.Keep;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.CitraApplication;
|
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.activities.EmulationActivity;
|
|
||||||
import org.citra.citra_emu.utils.Log;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
public final class SoftwareKeyboard {
|
|
||||||
/// Corresponds to Frontend::ButtonConfig
|
|
||||||
private interface ButtonConfig {
|
|
||||||
int Single = 0; /// Ok button
|
|
||||||
int Dual = 1; /// Cancel | Ok buttons
|
|
||||||
int Triple = 2; /// Cancel | I Forgot | Ok buttons
|
|
||||||
int None = 3; /// No button (returned by swkbdInputText in special cases)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Corresponds to Frontend::ValidationError
|
|
||||||
public enum ValidationError {
|
|
||||||
None,
|
|
||||||
// Button Selection
|
|
||||||
ButtonOutOfRange,
|
|
||||||
// Configured Filters
|
|
||||||
MaxDigitsExceeded,
|
|
||||||
AtSignNotAllowed,
|
|
||||||
PercentNotAllowed,
|
|
||||||
BackslashNotAllowed,
|
|
||||||
ProfanityNotAllowed,
|
|
||||||
CallbackFailed,
|
|
||||||
// Allowed Input Type
|
|
||||||
FixedLengthRequired,
|
|
||||||
MaxLengthExceeded,
|
|
||||||
BlankInputNotAllowed,
|
|
||||||
EmptyInputNotAllowed,
|
|
||||||
}
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
public static class KeyboardConfig implements java.io.Serializable {
|
|
||||||
public int button_config;
|
|
||||||
public int max_text_length;
|
|
||||||
public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input
|
|
||||||
public String hint_text; /// Displayed in the field as a hint before
|
|
||||||
@Nullable
|
|
||||||
public String[] button_text; /// Contains the button text that the caller provides
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Corresponds to Frontend::KeyboardData
|
|
||||||
public static class KeyboardData {
|
|
||||||
public int button;
|
|
||||||
public String text;
|
|
||||||
|
|
||||||
private KeyboardData(int button, String text) {
|
|
||||||
this.button = button;
|
|
||||||
this.text = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Filter implements InputFilter {
|
|
||||||
@Override
|
|
||||||
public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
|
|
||||||
int dstart, int dend) {
|
|
||||||
String text = new StringBuilder(dest)
|
|
||||||
.replace(dstart, dend, source.subSequence(start, end).toString())
|
|
||||||
.toString();
|
|
||||||
if (ValidateFilters(text) == ValidationError.None) {
|
|
||||||
return null; // Accept replacement
|
|
||||||
}
|
|
||||||
return dest.subSequence(dstart, dend); // Request the subsequence to be unchanged
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class KeyboardDialogFragment extends DialogFragment {
|
|
||||||
static KeyboardDialogFragment newInstance(KeyboardConfig config) {
|
|
||||||
KeyboardDialogFragment frag = new KeyboardDialogFragment();
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putSerializable("config", config);
|
|
||||||
frag.setArguments(args);
|
|
||||||
return frag;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
final Activity emulationActivity = getActivity();
|
|
||||||
assert emulationActivity != null;
|
|
||||||
|
|
||||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
||||||
params.leftMargin = params.rightMargin =
|
|
||||||
CitraApplication.Companion.getAppContext().getResources().getDimensionPixelSize(
|
|
||||||
R.dimen.dialog_margin);
|
|
||||||
|
|
||||||
KeyboardConfig config = Objects.requireNonNull(
|
|
||||||
(KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config"));
|
|
||||||
|
|
||||||
// Set up the input
|
|
||||||
EditText editText = new EditText(CitraApplication.Companion.getAppContext());
|
|
||||||
editText.setHint(config.hint_text);
|
|
||||||
editText.setSingleLine(!config.multiline_mode);
|
|
||||||
editText.setLayoutParams(params);
|
|
||||||
editText.setFilters(new InputFilter[]{
|
|
||||||
new Filter(), new InputFilter.LengthFilter(config.max_text_length)});
|
|
||||||
|
|
||||||
TypedValue typedValue = new TypedValue();
|
|
||||||
Resources.Theme theme = requireContext().getTheme();
|
|
||||||
theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true);
|
|
||||||
@ColorInt int color = typedValue.data;
|
|
||||||
editText.setHintTextColor(color);
|
|
||||||
editText.setTextColor(color);
|
|
||||||
|
|
||||||
FrameLayout container = new FrameLayout(emulationActivity);
|
|
||||||
container.addView(editText);
|
|
||||||
|
|
||||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
|
||||||
.setTitle(R.string.software_keyboard)
|
|
||||||
.setView(container);
|
|
||||||
setCancelable(false);
|
|
||||||
|
|
||||||
switch (config.button_config) {
|
|
||||||
case ButtonConfig.Triple: {
|
|
||||||
final String text = config.button_text[1].isEmpty()
|
|
||||||
? emulationActivity.getString(R.string.i_forgot)
|
|
||||||
: config.button_text[1];
|
|
||||||
builder.setNeutralButton(text, null);
|
|
||||||
}
|
|
||||||
// fallthrough
|
|
||||||
case ButtonConfig.Dual: {
|
|
||||||
final String text = config.button_text[0].isEmpty()
|
|
||||||
? emulationActivity.getString(android.R.string.cancel)
|
|
||||||
: config.button_text[0];
|
|
||||||
builder.setNegativeButton(text, null);
|
|
||||||
}
|
|
||||||
// fallthrough
|
|
||||||
case ButtonConfig.Single: {
|
|
||||||
final String text = config.button_text[2].isEmpty()
|
|
||||||
? emulationActivity.getString(android.R.string.ok)
|
|
||||||
: config.button_text[2];
|
|
||||||
builder.setPositiveButton(text, null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final AlertDialog dialog = builder.create();
|
|
||||||
dialog.create();
|
|
||||||
if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) {
|
|
||||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> {
|
|
||||||
data.button = config.button_config;
|
|
||||||
data.text = editText.getText().toString();
|
|
||||||
final ValidationError error = ValidateInput(data.text);
|
|
||||||
if (error != ValidationError.None) {
|
|
||||||
HandleValidationError(config, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.dismiss();
|
|
||||||
|
|
||||||
synchronized (finishLock) {
|
|
||||||
finishLock.notifyAll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) {
|
|
||||||
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> {
|
|
||||||
data.button = 1;
|
|
||||||
dialog.dismiss();
|
|
||||||
synchronized (finishLock) {
|
|
||||||
finishLock.notifyAll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) {
|
|
||||||
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> {
|
|
||||||
data.button = 0;
|
|
||||||
dialog.dismiss();
|
|
||||||
synchronized (finishLock) {
|
|
||||||
finishLock.notifyAll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static KeyboardData data;
|
|
||||||
private static final Object finishLock = new Object();
|
|
||||||
|
|
||||||
private static void ExecuteImpl(KeyboardConfig config) {
|
|
||||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
|
||||||
|
|
||||||
data = new KeyboardData(0, "");
|
|
||||||
|
|
||||||
KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config);
|
|
||||||
fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void HandleValidationError(KeyboardConfig config, ValidationError error) {
|
|
||||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
|
||||||
String message = "";
|
|
||||||
switch (error) {
|
|
||||||
case FixedLengthRequired:
|
|
||||||
message =
|
|
||||||
emulationActivity.getString(R.string.fixed_length_required, config.max_text_length);
|
|
||||||
break;
|
|
||||||
case MaxLengthExceeded:
|
|
||||||
message =
|
|
||||||
emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length);
|
|
||||||
break;
|
|
||||||
case BlankInputNotAllowed:
|
|
||||||
message = emulationActivity.getString(R.string.blank_input_not_allowed);
|
|
||||||
break;
|
|
||||||
case EmptyInputNotAllowed:
|
|
||||||
message = emulationActivity.getString(R.string.empty_input_not_allowed);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
new MaterialAlertDialogBuilder(emulationActivity)
|
|
||||||
.setTitle(R.string.software_keyboard)
|
|
||||||
.setMessage(message)
|
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static KeyboardData Execute(KeyboardConfig config) {
|
|
||||||
if (config.button_config == ButtonConfig.None) {
|
|
||||||
Log.error("Unexpected button config None");
|
|
||||||
return new KeyboardData(0, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
|
|
||||||
|
|
||||||
synchronized (finishLock) {
|
|
||||||
try {
|
|
||||||
finishLock.wait();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ShowError(String error) {
|
|
||||||
NativeLibrary.displayAlertMsg(
|
|
||||||
CitraApplication.Companion.getAppContext().getResources().getString(R.string.software_keyboard),
|
|
||||||
error, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static native ValidationError ValidateFilters(String text);
|
|
||||||
|
|
||||||
private static native ValidationError ValidateInput(String text);
|
|
||||||
}
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.applets
|
||||||
|
|
||||||
|
import android.text.InputFilter
|
||||||
|
import android.text.Spanned
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import org.citra.citra_emu.CitraApplication.Companion.appContext
|
||||||
|
import org.citra.citra_emu.NativeLibrary
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.fragments.KeyboardDialogFragment
|
||||||
|
import org.citra.citra_emu.fragments.MessageDialogFragment
|
||||||
|
import org.citra.citra_emu.utils.Log
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
object SoftwareKeyboard {
|
||||||
|
lateinit var data: KeyboardData
|
||||||
|
val finishLock = Object()
|
||||||
|
|
||||||
|
private fun ExecuteImpl(config: KeyboardConfig) {
|
||||||
|
val emulationActivity = NativeLibrary.sEmulationActivity.get()
|
||||||
|
data = KeyboardData(0, "")
|
||||||
|
KeyboardDialogFragment.newInstance(config)
|
||||||
|
.show(emulationActivity!!.supportFragmentManager, KeyboardDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun HandleValidationError(config: KeyboardConfig, error: ValidationError) {
|
||||||
|
val emulationActivity = NativeLibrary.sEmulationActivity.get()!!
|
||||||
|
val message: String = when (error) {
|
||||||
|
ValidationError.FixedLengthRequired -> emulationActivity.getString(
|
||||||
|
R.string.fixed_length_required,
|
||||||
|
config.maxTextLength
|
||||||
|
)
|
||||||
|
|
||||||
|
ValidationError.MaxLengthExceeded ->
|
||||||
|
emulationActivity.getString(R.string.max_length_exceeded, config.maxTextLength)
|
||||||
|
|
||||||
|
ValidationError.BlankInputNotAllowed ->
|
||||||
|
emulationActivity.getString(R.string.blank_input_not_allowed)
|
||||||
|
|
||||||
|
ValidationError.EmptyInputNotAllowed ->
|
||||||
|
emulationActivity.getString(R.string.empty_input_not_allowed)
|
||||||
|
|
||||||
|
else -> emulationActivity.getString(R.string.invalid_input)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialogFragment.newInstance(R.string.software_keyboard, message).show(
|
||||||
|
NativeLibrary.sEmulationActivity.get()!!.supportFragmentManager,
|
||||||
|
MessageDialogFragment.TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun Execute(config: KeyboardConfig): KeyboardData {
|
||||||
|
if (config.buttonConfig == ButtonConfig.None) {
|
||||||
|
Log.error("Unexpected button config None")
|
||||||
|
return KeyboardData(0, "")
|
||||||
|
}
|
||||||
|
NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { ExecuteImpl(config) }
|
||||||
|
synchronized(finishLock) {
|
||||||
|
try {
|
||||||
|
finishLock.wait()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun ShowError(error: String) {
|
||||||
|
NativeLibrary.displayAlertMsg(
|
||||||
|
appContext.resources.getString(R.string.software_keyboard),
|
||||||
|
error,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private external fun ValidateFilters(text: String): ValidationError
|
||||||
|
external fun ValidateInput(text: String): ValidationError
|
||||||
|
|
||||||
|
/// Corresponds to Frontend::ButtonConfig
|
||||||
|
interface ButtonConfig {
|
||||||
|
companion object {
|
||||||
|
const val Single = 0 /// Ok button
|
||||||
|
const val Dual = 1 /// Cancel | Ok buttons
|
||||||
|
const val Triple = 2 /// Cancel | I Forgot | Ok buttons
|
||||||
|
const val None = 3 /// No button (returned by swkbdInputText in special cases)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Corresponds to Frontend::ValidationError
|
||||||
|
enum class ValidationError {
|
||||||
|
None,
|
||||||
|
|
||||||
|
// Button Selection
|
||||||
|
ButtonOutOfRange,
|
||||||
|
|
||||||
|
// Configured Filters
|
||||||
|
MaxDigitsExceeded,
|
||||||
|
AtSignNotAllowed,
|
||||||
|
PercentNotAllowed,
|
||||||
|
BackslashNotAllowed,
|
||||||
|
ProfanityNotAllowed,
|
||||||
|
CallbackFailed,
|
||||||
|
|
||||||
|
// Allowed Input Type
|
||||||
|
FixedLengthRequired,
|
||||||
|
MaxLengthExceeded,
|
||||||
|
BlankInputNotAllowed,
|
||||||
|
EmptyInputNotAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
class KeyboardConfig : Serializable {
|
||||||
|
var buttonConfig = 0
|
||||||
|
var maxTextLength = 0
|
||||||
|
|
||||||
|
// True if the keyboard accepts multiple lines of input
|
||||||
|
var multilineMode = false
|
||||||
|
|
||||||
|
// Displayed in the field as a hint before
|
||||||
|
var hintText: String? = null
|
||||||
|
|
||||||
|
// Contains the button text that the caller provides
|
||||||
|
lateinit var buttonText: Array<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Corresponds to Frontend::KeyboardData
|
||||||
|
class KeyboardData(var button: Int, var text: String)
|
||||||
|
class Filter : InputFilter {
|
||||||
|
override fun filter(
|
||||||
|
source: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
dest: Spanned,
|
||||||
|
dstart: Int,
|
||||||
|
dend: Int
|
||||||
|
): CharSequence? {
|
||||||
|
val text = StringBuilder(dest)
|
||||||
|
.replace(dstart, dend, source.subSequence(start, end).toString())
|
||||||
|
.toString()
|
||||||
|
return if (ValidateFilters(text) == ValidationError.None) {
|
||||||
|
null // Accept replacement
|
||||||
|
} else {
|
||||||
|
dest.subSequence(dstart, dend) // Request the subsequence to be unchanged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,68 +0,0 @@
|
||||||
// Copyright 2020 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
package org.citra.citra_emu.camera;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.activities.EmulationActivity;
|
|
||||||
import org.citra.citra_emu.utils.PicassoUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
// Used in native code.
|
|
||||||
public final class StillImageCameraHelper {
|
|
||||||
public static final int REQUEST_CAMERA_FILE_PICKER = 1;
|
|
||||||
private static final Object filePickerLock = new Object();
|
|
||||||
private static @Nullable
|
|
||||||
String filePickerPath;
|
|
||||||
|
|
||||||
// Opens file picker for camera.
|
|
||||||
@Keep
|
|
||||||
public static @Nullable
|
|
||||||
String OpenFilePicker() {
|
|
||||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
|
||||||
|
|
||||||
// At this point, we are assuming that we already have permissions as they are
|
|
||||||
// needed to launch a game
|
|
||||||
emulationActivity.runOnUiThread(() -> {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_PICK);
|
|
||||||
intent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
|
||||||
emulationActivity.startActivityForResult(
|
|
||||||
Intent.createChooser(intent,
|
|
||||||
emulationActivity.getString(R.string.camera_select_image)),
|
|
||||||
REQUEST_CAMERA_FILE_PICKER);
|
|
||||||
});
|
|
||||||
|
|
||||||
synchronized (filePickerLock) {
|
|
||||||
try {
|
|
||||||
filePickerLock.wait();
|
|
||||||
} catch (InterruptedException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filePickerPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called from EmulationActivity.
|
|
||||||
public static void OnFilePickerResult(Intent result) {
|
|
||||||
filePickerPath = result == null ? null : result.getDataString();
|
|
||||||
|
|
||||||
synchronized (filePickerLock) {
|
|
||||||
filePickerLock.notifyAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blocking call. Load image from file and crop/resize it to fit in width x height.
|
|
||||||
@Keep
|
|
||||||
@Nullable
|
|
||||||
public static Bitmap LoadImageFromFile(String uri, int width, int height) {
|
|
||||||
return PicassoUtils.LoadBitmapFromFile(uri, width, height);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.camera
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import coil.executeBlocking
|
||||||
|
import coil.imageLoader
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import org.citra.citra_emu.CitraApplication
|
||||||
|
import org.citra.citra_emu.NativeLibrary
|
||||||
|
|
||||||
|
// Used in native code.
|
||||||
|
object StillImageCameraHelper {
|
||||||
|
private val filePickerLock = Object()
|
||||||
|
private var filePickerPath: String? = null
|
||||||
|
|
||||||
|
// Opens file picker for camera.
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun OpenFilePicker(): String? {
|
||||||
|
val emulationActivity = NativeLibrary.sEmulationActivity.get()
|
||||||
|
|
||||||
|
// At this point, we are assuming that we already have permissions as they are
|
||||||
|
// needed to launch a game
|
||||||
|
emulationActivity!!.runOnUiThread {
|
||||||
|
val request = PickVisualMediaRequest.Builder()
|
||||||
|
.setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly).build()
|
||||||
|
emulationActivity.openImageLauncher.launch(request)
|
||||||
|
}
|
||||||
|
synchronized(filePickerLock) {
|
||||||
|
try {
|
||||||
|
filePickerLock.wait()
|
||||||
|
} catch (ignored: InterruptedException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filePickerPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from EmulationActivity.
|
||||||
|
@JvmStatic
|
||||||
|
fun OnFilePickerResult(result: String) {
|
||||||
|
filePickerPath = result
|
||||||
|
synchronized(filePickerLock) { filePickerLock.notifyAll() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocking call. Load image from file and crop/resize it to fit in width x height.
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun LoadImageFromFile(uri: String?, width: Int, height: Int): Bitmap? {
|
||||||
|
val context = CitraApplication.appContext
|
||||||
|
val request = ImageRequest.Builder(context)
|
||||||
|
.data(uri)
|
||||||
|
.size(width, height)
|
||||||
|
.build()
|
||||||
|
return context.imageLoader.executeBlocking(request).drawable?.toBitmap(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +0,0 @@
|
||||||
package org.citra.citra_emu.contracts;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.util.Pair;
|
|
||||||
|
|
||||||
import androidx.activity.result.contract.ActivityResultContract;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
public class OpenFileResultContract extends ActivityResultContract<Boolean, Intent> {
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Intent createIntent(@NonNull Context context, Boolean allowMultiple) {
|
|
||||||
return new Intent(Intent.ACTION_OPEN_DOCUMENT)
|
|
||||||
.setType("application/octet-stream")
|
|
||||||
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Intent parseResult(int i, @Nullable Intent intent) {
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.contracts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
|
||||||
|
class OpenFileResultContract : ActivityResultContract<Boolean?, Intent?>() {
|
||||||
|
override fun createIntent(context: Context, input: Boolean?): Intent {
|
||||||
|
return Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
|
.setType("application/octet-stream")
|
||||||
|
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseResult(resultCode: Int, intent: Intent?): Intent? = intent
|
||||||
|
}
|
|
@ -1,140 +0,0 @@
|
||||||
package org.citra.citra_emu.dialogs;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.InputDevice;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
|
||||||
import org.citra.citra_emu.utils.Log;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link AlertDialog} derivative that listens for
|
|
||||||
* motion events from controllers and joysticks.
|
|
||||||
*/
|
|
||||||
public final class MotionAlertDialog extends AlertDialog {
|
|
||||||
// The selected input preference
|
|
||||||
private final InputBindingSetting setting;
|
|
||||||
private final ArrayList<Float> mPreviousValues = new ArrayList<>();
|
|
||||||
private int mPrevDeviceId = 0;
|
|
||||||
private boolean mWaitingForEvent = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param context The current {@link Context}.
|
|
||||||
* @param setting The Preference to show this dialog for.
|
|
||||||
*/
|
|
||||||
public MotionAlertDialog(Context context, InputBindingSetting setting) {
|
|
||||||
super(context);
|
|
||||||
|
|
||||||
this.setting = setting;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean onKeyEvent(int keyCode, KeyEvent event) {
|
|
||||||
Log.debug("[MotionAlertDialog] Received key event: " + event.getAction());
|
|
||||||
switch (event.getAction()) {
|
|
||||||
case KeyEvent.ACTION_UP:
|
|
||||||
setting.onKeyInput(event);
|
|
||||||
dismiss();
|
|
||||||
// Even if we ignore the key, we still consume it. Thus return true regardless.
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) {
|
|
||||||
return super.onKeyLongPress(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
|
||||||
// Handle this key if we care about it, otherwise pass it down the framework
|
|
||||||
return onKeyEvent(event.getKeyCode(), event) || super.dispatchKeyEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) {
|
|
||||||
// Handle this event if we care about it, otherwise pass it down the framework
|
|
||||||
return onMotionEvent(event) || super.dispatchGenericMotionEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean onMotionEvent(MotionEvent event) {
|
|
||||||
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)
|
|
||||||
return false;
|
|
||||||
if (event.getAction() != MotionEvent.ACTION_MOVE)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
InputDevice input = event.getDevice();
|
|
||||||
|
|
||||||
List<InputDevice.MotionRange> motionRanges = input.getMotionRanges();
|
|
||||||
|
|
||||||
if (input.getId() != mPrevDeviceId) {
|
|
||||||
mPreviousValues.clear();
|
|
||||||
}
|
|
||||||
mPrevDeviceId = input.getId();
|
|
||||||
boolean firstEvent = mPreviousValues.isEmpty();
|
|
||||||
|
|
||||||
int numMovedAxis = 0;
|
|
||||||
float axisMoveValue = 0.0f;
|
|
||||||
InputDevice.MotionRange lastMovedRange = null;
|
|
||||||
char lastMovedDir = '?';
|
|
||||||
if (mWaitingForEvent) {
|
|
||||||
for (int i = 0; i < motionRanges.size(); i++) {
|
|
||||||
InputDevice.MotionRange range = motionRanges.get(i);
|
|
||||||
int axis = range.getAxis();
|
|
||||||
float origValue = event.getAxisValue(axis);
|
|
||||||
float value = origValue;//ControllerMappingHelper.scaleAxis(input, axis, origValue);
|
|
||||||
if (firstEvent) {
|
|
||||||
mPreviousValues.add(value);
|
|
||||||
} else {
|
|
||||||
float previousValue = mPreviousValues.get(i);
|
|
||||||
|
|
||||||
// Only handle the axes that are not neutral (more than 0.5)
|
|
||||||
// but ignore any axis that has a constant value (e.g. always 1)
|
|
||||||
if (Math.abs(value) > 0.5f && value != previousValue) {
|
|
||||||
// It is common to have multiple axes with the same physical input. For example,
|
|
||||||
// shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE.
|
|
||||||
// To handle this, we ignore an axis motion that's the exact same as a motion
|
|
||||||
// we already saw. This way, we ignore axes with two names, but catch the case
|
|
||||||
// where a joystick is moved in two directions.
|
|
||||||
// ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html
|
|
||||||
if (value != axisMoveValue) {
|
|
||||||
axisMoveValue = value;
|
|
||||||
numMovedAxis++;
|
|
||||||
lastMovedRange = range;
|
|
||||||
lastMovedDir = value < 0.0f ? '-' : '+';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Special case for d-pads (axis value jumps between 0 and 1 without any values
|
|
||||||
// in between). Without this, the user would need to press the d-pad twice
|
|
||||||
// due to the first press being caught by the "if (firstEvent)" case further up.
|
|
||||||
else if (Math.abs(value) < 0.25f && Math.abs(previousValue) > 0.75f) {
|
|
||||||
numMovedAxis++;
|
|
||||||
lastMovedRange = range;
|
|
||||||
lastMovedDir = previousValue < 0.0f ? '-' : '+';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mPreviousValues.set(i, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If only one axis moved, that's the winner.
|
|
||||||
if (numMovedAxis == 1) {
|
|
||||||
mWaitingForEvent = false;
|
|
||||||
setting.onMotionInput(input, lastMovedRange, lastMovedDir);
|
|
||||||
dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.display
|
||||||
|
|
||||||
|
import android.view.WindowManager
|
||||||
|
import org.citra.citra_emu.NativeLibrary
|
||||||
|
import org.citra.citra_emu.features.settings.model.BooleanSetting
|
||||||
|
import org.citra.citra_emu.features.settings.model.IntSetting
|
||||||
|
import org.citra.citra_emu.features.settings.model.Settings
|
||||||
|
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||||
|
import org.citra.citra_emu.utils.EmulationMenuSettings
|
||||||
|
|
||||||
|
class ScreenAdjustmentUtil(private val windowManager: WindowManager,
|
||||||
|
private val settings: Settings) {
|
||||||
|
fun swapScreen() {
|
||||||
|
val isEnabled = !EmulationMenuSettings.swapScreens
|
||||||
|
EmulationMenuSettings.swapScreens = isEnabled
|
||||||
|
NativeLibrary.swapScreens(
|
||||||
|
isEnabled,
|
||||||
|
windowManager.defaultDisplay.rotation
|
||||||
|
)
|
||||||
|
BooleanSetting.SWAP_SCREEN.boolean = isEnabled
|
||||||
|
settings.saveSetting(BooleanSetting.SWAP_SCREEN, SettingsFile.FILE_NAME_CONFIG)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cycleLayouts() {
|
||||||
|
val nextLayout = (EmulationMenuSettings.landscapeScreenLayout + 1) % ScreenLayout.entries.size
|
||||||
|
changeScreenOrientation(ScreenLayout.from(nextLayout))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeScreenOrientation(layoutOption: ScreenLayout) {
|
||||||
|
EmulationMenuSettings.landscapeScreenLayout = layoutOption.int
|
||||||
|
NativeLibrary.notifyOrientationChange(
|
||||||
|
EmulationMenuSettings.landscapeScreenLayout,
|
||||||
|
windowManager.defaultDisplay.rotation
|
||||||
|
)
|
||||||
|
IntSetting.SCREEN_LAYOUT.int = layoutOption.int
|
||||||
|
settings.saveSetting(IntSetting.SCREEN_LAYOUT, SettingsFile.FILE_NAME_CONFIG)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.display
|
||||||
|
|
||||||
|
enum class ScreenLayout(val int: Int) {
|
||||||
|
// These must match what is defined in src/common/settings.h
|
||||||
|
DEFAULT(0),
|
||||||
|
SINGLE_SCREEN(1),
|
||||||
|
LARGE_SCREEN(2),
|
||||||
|
SIDE_SCREEN(3),
|
||||||
|
HYBRID_SCREEN(4),
|
||||||
|
MOBILE_PORTRAIT(5),
|
||||||
|
MOBILE_LANDSCAPE(6);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(int: Int): ScreenLayout {
|
||||||
|
return entries.firstOrNull { it.int == int } ?: DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,57 +0,0 @@
|
||||||
package org.citra.citra_emu.features.cheats.model;
|
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
public class Cheat {
|
|
||||||
@Keep
|
|
||||||
private final long mPointer;
|
|
||||||
|
|
||||||
private Runnable mEnabledChangedCallback = null;
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
private Cheat(long pointer) {
|
|
||||||
mPointer = pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected native void finalize();
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public native String getName();
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public native String getNotes();
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public native String getCode();
|
|
||||||
|
|
||||||
public native boolean getEnabled();
|
|
||||||
|
|
||||||
public void setEnabled(boolean enabled) {
|
|
||||||
setEnabledImpl(enabled);
|
|
||||||
onEnabledChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void setEnabledImpl(boolean enabled);
|
|
||||||
|
|
||||||
public void setEnabledChangedCallback(@Nullable Runnable callback) {
|
|
||||||
mEnabledChangedCallback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onEnabledChanged() {
|
|
||||||
if (mEnabledChangedCallback != null) {
|
|
||||||
mEnabledChangedCallback.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the code is valid, returns 0. Otherwise, returns the 1-based index
|
|
||||||
* for the line containing the error.
|
|
||||||
*/
|
|
||||||
public static native int isValidGatewayCode(@NonNull String code);
|
|
||||||
|
|
||||||
public static native Cheat createGatewayCode(@NonNull String name, @NonNull String notes,
|
|
||||||
@NonNull String code);
|
|
||||||
}
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.cheats.model
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
class Cheat(@field:Keep private val mPointer: Long) {
|
||||||
|
private var enabledChangedCallback: Runnable? = null
|
||||||
|
protected external fun finalize()
|
||||||
|
|
||||||
|
external fun getName(): String
|
||||||
|
|
||||||
|
external fun getNotes(): String
|
||||||
|
|
||||||
|
external fun getCode(): String
|
||||||
|
|
||||||
|
external fun getEnabled(): Boolean
|
||||||
|
|
||||||
|
fun setEnabled(enabled: Boolean) {
|
||||||
|
setEnabledImpl(enabled)
|
||||||
|
onEnabledChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
private external fun setEnabledImpl(enabled: Boolean)
|
||||||
|
|
||||||
|
fun setEnabledChangedCallback(callback: Runnable) {
|
||||||
|
enabledChangedCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEnabledChanged() {
|
||||||
|
enabledChangedCallback?.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* If the code is valid, returns 0. Otherwise, returns the 1-based index
|
||||||
|
* for the line containing the error.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
external fun isValidGatewayCode(code: String): Int
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
external fun createGatewayCode(name: String, notes: String, code: String): Cheat
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,28 +0,0 @@
|
||||||
package org.citra.citra_emu.features.cheats.model;
|
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
|
||||||
|
|
||||||
public class CheatEngine {
|
|
||||||
@Keep
|
|
||||||
private final long mPointer;
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
public CheatEngine(long titleId) {
|
|
||||||
mPointer = initialize(titleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static native long initialize(long titleId);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected native void finalize();
|
|
||||||
|
|
||||||
public native Cheat[] getCheats();
|
|
||||||
|
|
||||||
public native void addCheat(Cheat cheat);
|
|
||||||
|
|
||||||
public native void removeCheat(int index);
|
|
||||||
|
|
||||||
public native void updateCheat(int index, Cheat newCheat);
|
|
||||||
|
|
||||||
public native void saveCheatFile();
|
|
||||||
}
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.cheats.model
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
object CheatEngine {
|
||||||
|
external fun loadCheatFile(titleId: Long)
|
||||||
|
external fun saveCheatFile(titleId: Long)
|
||||||
|
|
||||||
|
external fun getCheats(): Array<Cheat>
|
||||||
|
|
||||||
|
external fun addCheat(cheat: Cheat?)
|
||||||
|
external fun removeCheat(index: Int)
|
||||||
|
external fun updateCheat(index: Int, newCheat: Cheat?)
|
||||||
|
}
|
|
@ -1,187 +0,0 @@
|
||||||
package org.citra.citra_emu.features.cheats.model;
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
import androidx.lifecycle.ViewModel;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
public class CheatsViewModel extends ViewModel {
|
|
||||||
private int mSelectedCheatPosition = -1;
|
|
||||||
private final MutableLiveData<Cheat> mSelectedCheat = new MutableLiveData<>(null);
|
|
||||||
private final MutableLiveData<Boolean> mIsAdding = new MutableLiveData<>(false);
|
|
||||||
private final MutableLiveData<Boolean> mIsEditing = new MutableLiveData<>(false);
|
|
||||||
|
|
||||||
private final MutableLiveData<Integer> mCheatAddedEvent = new MutableLiveData<>(null);
|
|
||||||
private final MutableLiveData<Integer> mCheatChangedEvent = new MutableLiveData<>(null);
|
|
||||||
private final MutableLiveData<Integer> mCheatDeletedEvent = new MutableLiveData<>(null);
|
|
||||||
private final MutableLiveData<Boolean> mOpenDetailsViewEvent = new MutableLiveData<>(false);
|
|
||||||
|
|
||||||
private CheatEngine mCheatEngine;
|
|
||||||
private Cheat[] mCheats;
|
|
||||||
private boolean mCheatsNeedSaving = false;
|
|
||||||
|
|
||||||
public void initialize(long titleId) {
|
|
||||||
mCheatEngine = new CheatEngine(titleId);
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void load() {
|
|
||||||
mCheats = mCheatEngine.getCheats();
|
|
||||||
|
|
||||||
for (int i = 0; i < mCheats.length; i++) {
|
|
||||||
int position = i;
|
|
||||||
mCheats[i].setEnabledChangedCallback(() -> {
|
|
||||||
mCheatsNeedSaving = true;
|
|
||||||
notifyCheatUpdated(position);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void saveIfNeeded() {
|
|
||||||
if (mCheatsNeedSaving) {
|
|
||||||
mCheatEngine.saveCheatFile();
|
|
||||||
mCheatsNeedSaving = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Cheat[] getCheats() {
|
|
||||||
return mCheats;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LiveData<Cheat> getSelectedCheat() {
|
|
||||||
return mSelectedCheat;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSelectedCheat(Cheat cheat, int position) {
|
|
||||||
if (mIsEditing.getValue()) {
|
|
||||||
setIsEditing(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
mSelectedCheat.setValue(cheat);
|
|
||||||
mSelectedCheatPosition = position;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LiveData<Boolean> getIsAdding() {
|
|
||||||
return mIsAdding;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LiveData<Boolean> getIsEditing() {
|
|
||||||
return mIsEditing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIsEditing(boolean isEditing) {
|
|
||||||
mIsEditing.setValue(isEditing);
|
|
||||||
|
|
||||||
if (mIsAdding.getValue() && !isEditing) {
|
|
||||||
mIsAdding.setValue(false);
|
|
||||||
setSelectedCheat(null, -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a cheat is added, the integer stored in the returned LiveData
|
|
||||||
* changes to the position of that cheat, then changes back to null.
|
|
||||||
*/
|
|
||||||
public LiveData<Integer> getCheatAddedEvent() {
|
|
||||||
return mCheatAddedEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyCheatAdded(int position) {
|
|
||||||
mCheatAddedEvent.setValue(position);
|
|
||||||
mCheatAddedEvent.setValue(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startAddingCheat() {
|
|
||||||
mSelectedCheat.setValue(null);
|
|
||||||
mSelectedCheatPosition = -1;
|
|
||||||
|
|
||||||
mIsAdding.setValue(true);
|
|
||||||
mIsEditing.setValue(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void finishAddingCheat(Cheat cheat) {
|
|
||||||
if (!mIsAdding.getValue()) {
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
mIsAdding.setValue(false);
|
|
||||||
mIsEditing.setValue(false);
|
|
||||||
|
|
||||||
int position = mCheats.length;
|
|
||||||
|
|
||||||
mCheatEngine.addCheat(cheat);
|
|
||||||
|
|
||||||
mCheatsNeedSaving = true;
|
|
||||||
load();
|
|
||||||
|
|
||||||
notifyCheatAdded(position);
|
|
||||||
setSelectedCheat(mCheats[position], position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a cheat is edited, the integer stored in the returned LiveData
|
|
||||||
* changes to the position of that cheat, then changes back to null.
|
|
||||||
*/
|
|
||||||
public LiveData<Integer> getCheatUpdatedEvent() {
|
|
||||||
return mCheatChangedEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies that an edit has been made to the contents of the cheat at the given position.
|
|
||||||
*/
|
|
||||||
private void notifyCheatUpdated(int position) {
|
|
||||||
mCheatChangedEvent.setValue(position);
|
|
||||||
mCheatChangedEvent.setValue(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateSelectedCheat(Cheat newCheat) {
|
|
||||||
mCheatEngine.updateCheat(mSelectedCheatPosition, newCheat);
|
|
||||||
|
|
||||||
mCheatsNeedSaving = true;
|
|
||||||
load();
|
|
||||||
|
|
||||||
notifyCheatUpdated(mSelectedCheatPosition);
|
|
||||||
setSelectedCheat(mCheats[mSelectedCheatPosition], mSelectedCheatPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a cheat is deleted, the integer stored in the returned LiveData
|
|
||||||
* changes to the position of that cheat, then changes back to null.
|
|
||||||
*/
|
|
||||||
public LiveData<Integer> getCheatDeletedEvent() {
|
|
||||||
return mCheatDeletedEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies that the cheat at the given position has been deleted.
|
|
||||||
*/
|
|
||||||
private void notifyCheatDeleted(int position) {
|
|
||||||
mCheatDeletedEvent.setValue(position);
|
|
||||||
mCheatDeletedEvent.setValue(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteSelectedCheat() {
|
|
||||||
int position = mSelectedCheatPosition;
|
|
||||||
|
|
||||||
setSelectedCheat(null, -1);
|
|
||||||
|
|
||||||
mCheatEngine.removeCheat(position);
|
|
||||||
|
|
||||||
mCheatsNeedSaving = true;
|
|
||||||
load();
|
|
||||||
|
|
||||||
notifyCheatDeleted(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LiveData<Boolean> getOpenDetailsViewEvent() {
|
|
||||||
return mOpenDetailsViewEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void openDetailsView() {
|
|
||||||
mOpenDetailsViewEvent.setValue(true);
|
|
||||||
mOpenDetailsViewEvent.setValue(false);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.cheats.model
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
class CheatsViewModel : ViewModel() {
|
||||||
|
val selectedCheat get() = _selectedCheat.asStateFlow()
|
||||||
|
private val _selectedCheat = MutableStateFlow<Cheat?>(null)
|
||||||
|
|
||||||
|
val isAdding get() = _isAdding.asStateFlow()
|
||||||
|
private val _isAdding = MutableStateFlow(false)
|
||||||
|
|
||||||
|
val isEditing get() = _isEditing.asStateFlow()
|
||||||
|
private val _isEditing = MutableStateFlow(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a cheat is added, the integer stored in the returned StateFlow
|
||||||
|
* changes to the position of that cheat, then changes back to null.
|
||||||
|
*/
|
||||||
|
val cheatAddedEvent get() = _cheatAddedEvent.asStateFlow()
|
||||||
|
private val _cheatAddedEvent = MutableStateFlow<Int?>(null)
|
||||||
|
|
||||||
|
val cheatChangedEvent get() = _cheatChangedEvent.asStateFlow()
|
||||||
|
private val _cheatChangedEvent = MutableStateFlow<Int?>(null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a cheat is deleted, the integer stored in the returned StateFlow
|
||||||
|
* changes to the position of that cheat, then changes back to null.
|
||||||
|
*/
|
||||||
|
val cheatDeletedEvent get() = _cheatDeletedEvent.asStateFlow()
|
||||||
|
private val _cheatDeletedEvent = MutableStateFlow<Int?>(null)
|
||||||
|
|
||||||
|
val openDetailsViewEvent get() = _openDetailsViewEvent.asStateFlow()
|
||||||
|
private val _openDetailsViewEvent = MutableStateFlow(false)
|
||||||
|
|
||||||
|
val closeDetailsViewEvent get() = _closeDetailsViewEvent.asStateFlow()
|
||||||
|
private val _closeDetailsViewEvent = MutableStateFlow(false)
|
||||||
|
|
||||||
|
val listViewFocusChange get() = _listViewFocusChange.asStateFlow()
|
||||||
|
private val _listViewFocusChange = MutableStateFlow(false)
|
||||||
|
|
||||||
|
val detailsViewFocusChange get() = _detailsViewFocusChange.asStateFlow()
|
||||||
|
private val _detailsViewFocusChange = MutableStateFlow(false)
|
||||||
|
|
||||||
|
private var titleId: Long = 0
|
||||||
|
lateinit var cheats: Array<Cheat>
|
||||||
|
private var cheatsNeedSaving = false
|
||||||
|
private var selectedCheatPosition = -1
|
||||||
|
|
||||||
|
fun initialize(titleId_: Long) {
|
||||||
|
titleId = titleId_;
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load() {
|
||||||
|
CheatEngine.loadCheatFile(titleId)
|
||||||
|
cheats = CheatEngine.getCheats()
|
||||||
|
for (i in cheats.indices) {
|
||||||
|
cheats[i].setEnabledChangedCallback {
|
||||||
|
cheatsNeedSaving = true
|
||||||
|
notifyCheatUpdated(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveIfNeeded() {
|
||||||
|
if (cheatsNeedSaving) {
|
||||||
|
CheatEngine.saveCheatFile(titleId)
|
||||||
|
cheatsNeedSaving = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSelectedCheat(cheat: Cheat?, position: Int) {
|
||||||
|
if (isEditing.value) {
|
||||||
|
setIsEditing(false)
|
||||||
|
}
|
||||||
|
_selectedCheat.value = cheat
|
||||||
|
selectedCheatPosition = position
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIsEditing(value: Boolean) {
|
||||||
|
_isEditing.value = value
|
||||||
|
if (isAdding.value && !value) {
|
||||||
|
_isAdding.value = false
|
||||||
|
setSelectedCheat(null, -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyCheatAdded(position: Int) {
|
||||||
|
_cheatAddedEvent.value = position
|
||||||
|
_cheatAddedEvent.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startAddingCheat() {
|
||||||
|
_selectedCheat.value = null
|
||||||
|
selectedCheatPosition = -1
|
||||||
|
_isAdding.value = true
|
||||||
|
_isEditing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finishAddingCheat(cheat: Cheat?) {
|
||||||
|
check(isAdding.value)
|
||||||
|
_isAdding.value = false
|
||||||
|
_isEditing.value = false
|
||||||
|
val position = cheats.size
|
||||||
|
CheatEngine.addCheat(cheat)
|
||||||
|
cheatsNeedSaving = true
|
||||||
|
load()
|
||||||
|
notifyCheatAdded(position)
|
||||||
|
setSelectedCheat(cheats[position], position)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies that an edit has been made to the contents of the cheat at the given position.
|
||||||
|
*/
|
||||||
|
private fun notifyCheatUpdated(position: Int) {
|
||||||
|
_cheatChangedEvent.value = position
|
||||||
|
_cheatChangedEvent.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSelectedCheat(newCheat: Cheat?) {
|
||||||
|
CheatEngine.updateCheat(selectedCheatPosition, newCheat)
|
||||||
|
cheatsNeedSaving = true
|
||||||
|
load()
|
||||||
|
notifyCheatUpdated(selectedCheatPosition)
|
||||||
|
setSelectedCheat(cheats[selectedCheatPosition], selectedCheatPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies that the cheat at the given position has been deleted.
|
||||||
|
*/
|
||||||
|
private fun notifyCheatDeleted(position: Int) {
|
||||||
|
_cheatDeletedEvent.value = position
|
||||||
|
_cheatDeletedEvent.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteSelectedCheat() {
|
||||||
|
val position = selectedCheatPosition
|
||||||
|
setSelectedCheat(null, -1)
|
||||||
|
CheatEngine.removeCheat(position)
|
||||||
|
cheatsNeedSaving = true
|
||||||
|
load()
|
||||||
|
notifyCheatDeleted(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openDetailsView() {
|
||||||
|
_openDetailsViewEvent.value = true
|
||||||
|
_openDetailsViewEvent.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeDetailsView() {
|
||||||
|
_closeDetailsViewEvent.value = true
|
||||||
|
_closeDetailsViewEvent.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onListViewFocusChanged(changed: Boolean) {
|
||||||
|
_listViewFocusChange.value = changed
|
||||||
|
_listViewFocusChange.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDetailsViewFocusChanged(changed: Boolean) {
|
||||||
|
_detailsViewFocusChange.value = changed
|
||||||
|
_detailsViewFocusChange.value = false
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,175 +0,0 @@
|
||||||
package org.citra.citra_emu.features.cheats.ui;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.ScrollView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.features.cheats.model.Cheat;
|
|
||||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
|
||||||
|
|
||||||
public class CheatDetailsFragment extends Fragment {
|
|
||||||
private View mRoot;
|
|
||||||
private ScrollView mScrollView;
|
|
||||||
private TextView mLabelName;
|
|
||||||
private EditText mEditName;
|
|
||||||
private EditText mEditNotes;
|
|
||||||
private EditText mEditCode;
|
|
||||||
private Button mButtonDelete;
|
|
||||||
private Button mButtonEdit;
|
|
||||||
private Button mButtonCancel;
|
|
||||||
private Button mButtonOk;
|
|
||||||
|
|
||||||
private CheatsViewModel mViewModel;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
|
||||||
@Nullable Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(R.layout.fragment_cheat_details, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
|
||||||
mRoot = view.findViewById(R.id.root);
|
|
||||||
mScrollView = view.findViewById(R.id.scroll_view);
|
|
||||||
mLabelName = view.findViewById(R.id.label_name);
|
|
||||||
mEditName = view.findViewById(R.id.edit_name);
|
|
||||||
mEditNotes = view.findViewById(R.id.edit_notes);
|
|
||||||
mEditCode = view.findViewById(R.id.edit_code);
|
|
||||||
mButtonDelete = view.findViewById(R.id.button_delete);
|
|
||||||
mButtonEdit = view.findViewById(R.id.button_edit);
|
|
||||||
mButtonCancel = view.findViewById(R.id.button_cancel);
|
|
||||||
mButtonOk = view.findViewById(R.id.button_ok);
|
|
||||||
|
|
||||||
CheatsActivity activity = (CheatsActivity) requireActivity();
|
|
||||||
mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
|
|
||||||
|
|
||||||
mViewModel.getSelectedCheat().observe(getViewLifecycleOwner(),
|
|
||||||
this::onSelectedCheatUpdated);
|
|
||||||
mViewModel.getIsEditing().observe(getViewLifecycleOwner(), this::onIsEditingUpdated);
|
|
||||||
|
|
||||||
mButtonDelete.setOnClickListener(this::onDeleteClicked);
|
|
||||||
mButtonEdit.setOnClickListener(this::onEditClicked);
|
|
||||||
mButtonCancel.setOnClickListener(this::onCancelClicked);
|
|
||||||
mButtonOk.setOnClickListener(this::onOkClicked);
|
|
||||||
|
|
||||||
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
|
|
||||||
// at the same time. If the user is navigating using a d-pad and moves focus to an element
|
|
||||||
// in the currently hidden pane, we need to manually show that pane.
|
|
||||||
CheatsActivity.setOnFocusChangeListenerRecursively(view,
|
|
||||||
(v, hasFocus) -> activity.onDetailsViewFocusChange(hasFocus));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearEditErrors() {
|
|
||||||
mEditName.setError(null);
|
|
||||||
mEditCode.setError(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onDeleteClicked(View view) {
|
|
||||||
String name = mEditName.getText().toString();
|
|
||||||
|
|
||||||
new MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setMessage(getString(R.string.cheats_delete_confirmation, name))
|
|
||||||
.setPositiveButton(android.R.string.yes,
|
|
||||||
(dialog, i) -> mViewModel.deleteSelectedCheat())
|
|
||||||
.setNegativeButton(android.R.string.no, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onEditClicked(View view) {
|
|
||||||
mViewModel.setIsEditing(true);
|
|
||||||
mButtonOk.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onCancelClicked(View view) {
|
|
||||||
mViewModel.setIsEditing(false);
|
|
||||||
onSelectedCheatUpdated(mViewModel.getSelectedCheat().getValue());
|
|
||||||
mButtonDelete.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onOkClicked(View view) {
|
|
||||||
clearEditErrors();
|
|
||||||
|
|
||||||
String name = mEditName.getText().toString();
|
|
||||||
String notes = mEditNotes.getText().toString();
|
|
||||||
String code = mEditCode.getText().toString();
|
|
||||||
|
|
||||||
if (name.isEmpty()) {
|
|
||||||
mEditName.setError(getString(R.string.cheats_error_no_name));
|
|
||||||
mScrollView.smoothScrollTo(0, mLabelName.getTop());
|
|
||||||
return;
|
|
||||||
} else if (code.isEmpty()) {
|
|
||||||
mEditCode.setError(getString(R.string.cheats_error_no_code_lines));
|
|
||||||
mScrollView.smoothScrollTo(0, mEditCode.getBottom());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int validityResult = Cheat.isValidGatewayCode(code);
|
|
||||||
|
|
||||||
if (validityResult != 0) {
|
|
||||||
mEditCode.setError(getString(R.string.cheats_error_on_line, validityResult));
|
|
||||||
mScrollView.smoothScrollTo(0, mEditCode.getBottom());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Cheat newCheat = Cheat.createGatewayCode(name, notes, code);
|
|
||||||
|
|
||||||
if (mViewModel.getIsAdding().getValue()) {
|
|
||||||
mViewModel.finishAddingCheat(newCheat);
|
|
||||||
} else {
|
|
||||||
mViewModel.updateSelectedCheat(newCheat);
|
|
||||||
}
|
|
||||||
|
|
||||||
mButtonEdit.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onSelectedCheatUpdated(@Nullable Cheat cheat) {
|
|
||||||
clearEditErrors();
|
|
||||||
|
|
||||||
boolean isEditing = mViewModel.getIsEditing().getValue();
|
|
||||||
|
|
||||||
mRoot.setVisibility(isEditing || cheat != null ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
// If the fragment was recreated while editing a cheat, it's vital that we
|
|
||||||
// don't repopulate the fields, otherwise the user's changes will be lost
|
|
||||||
if (!isEditing) {
|
|
||||||
if (cheat == null) {
|
|
||||||
mEditName.setText("");
|
|
||||||
mEditNotes.setText("");
|
|
||||||
mEditCode.setText("");
|
|
||||||
} else {
|
|
||||||
mEditName.setText(cheat.getName());
|
|
||||||
mEditNotes.setText(cheat.getNotes());
|
|
||||||
mEditCode.setText(cheat.getCode());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onIsEditingUpdated(boolean isEditing) {
|
|
||||||
if (isEditing) {
|
|
||||||
mRoot.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
mEditName.setEnabled(isEditing);
|
|
||||||
mEditNotes.setEnabled(isEditing);
|
|
||||||
mEditCode.setEnabled(isEditing);
|
|
||||||
|
|
||||||
mButtonDelete.setVisibility(isEditing ? View.GONE : View.VISIBLE);
|
|
||||||
mButtonEdit.setVisibility(isEditing ? View.GONE : View.VISIBLE);
|
|
||||||
mButtonCancel.setVisibility(isEditing ? View.VISIBLE : View.GONE);
|
|
||||||
mButtonOk.setVisibility(isEditing ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.cheats.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.databinding.FragmentCheatDetailsBinding
|
||||||
|
import org.citra.citra_emu.features.cheats.model.Cheat
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel
|
||||||
|
|
||||||
|
class CheatDetailsFragment : Fragment() {
|
||||||
|
private val cheatsViewModel: CheatsViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var _binding: FragmentCheatDetailsBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentCheatDetailsBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is using the correct scope, lint is just acting up
|
||||||
|
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
cheatsViewModel.selectedCheat.collect { onSelectedCheatUpdated(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
cheatsViewModel.isEditing.collect { onIsEditingUpdated(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.buttonDelete.setOnClickListener { onDeleteClicked() }
|
||||||
|
binding.buttonEdit.setOnClickListener { onEditClicked() }
|
||||||
|
binding.buttonCancel.setOnClickListener { onCancelClicked() }
|
||||||
|
binding.buttonOk.setOnClickListener { onOkClicked() }
|
||||||
|
|
||||||
|
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
|
||||||
|
// at the same time. If the user is navigating using a d-pad and moves focus to an element
|
||||||
|
// in the currently hidden pane, we need to manually show that pane.
|
||||||
|
CheatsActivity.setOnFocusChangeListenerRecursively(view) { _, hasFocus ->
|
||||||
|
cheatsViewModel.onDetailsViewFocusChanged(hasFocus)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.toolbarCheatDetails.setNavigationOnClickListener {
|
||||||
|
cheatsViewModel.closeDetailsView()
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearEditErrors() {
|
||||||
|
binding.editName.error = null
|
||||||
|
binding.editCode.error = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDeleteClicked() {
|
||||||
|
val name = binding.editNameInput.text.toString()
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setMessage(getString(R.string.cheats_delete_confirmation, name))
|
||||||
|
.setPositiveButton(
|
||||||
|
android.R.string.ok
|
||||||
|
) { _: DialogInterface?, _: Int -> cheatsViewModel.deleteSelectedCheat() }
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEditClicked() {
|
||||||
|
cheatsViewModel.setIsEditing(true)
|
||||||
|
binding.buttonOk.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCancelClicked() {
|
||||||
|
cheatsViewModel.setIsEditing(false)
|
||||||
|
onSelectedCheatUpdated(cheatsViewModel.selectedCheat.value)
|
||||||
|
binding.buttonDelete.requestFocus()
|
||||||
|
cheatsViewModel.closeDetailsView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onOkClicked() {
|
||||||
|
clearEditErrors()
|
||||||
|
val name = binding.editNameInput.text.toString()
|
||||||
|
val notes = binding.editNotesInput.text.toString()
|
||||||
|
val code = binding.editCodeInput.text.toString()
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
binding.editName.error = getString(R.string.cheats_error_no_name)
|
||||||
|
binding.scrollView.smoothScrollTo(0, binding.editNameInput.top)
|
||||||
|
return
|
||||||
|
} else if (code.isEmpty()) {
|
||||||
|
binding.editCode.error = getString(R.string.cheats_error_no_code_lines)
|
||||||
|
binding.scrollView.smoothScrollTo(0, binding.editCodeInput.bottom)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val validityResult = Cheat.isValidGatewayCode(code)
|
||||||
|
if (validityResult != 0) {
|
||||||
|
binding.editCode.error = getString(R.string.cheats_error_on_line, validityResult)
|
||||||
|
binding.scrollView.smoothScrollTo(0, binding.editCodeInput.bottom)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val newCheat = Cheat.createGatewayCode(name, notes, code)
|
||||||
|
if (cheatsViewModel.isAdding.value == true) {
|
||||||
|
cheatsViewModel.finishAddingCheat(newCheat)
|
||||||
|
} else {
|
||||||
|
cheatsViewModel.updateSelectedCheat(newCheat)
|
||||||
|
}
|
||||||
|
binding.buttonEdit.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSelectedCheatUpdated(cheat: Cheat?) {
|
||||||
|
clearEditErrors()
|
||||||
|
val isEditing: Boolean = cheatsViewModel.isEditing.value == true
|
||||||
|
|
||||||
|
// If the fragment was recreated while editing a cheat, it's vital that we
|
||||||
|
// don't repopulate the fields, otherwise the user's changes will be lost
|
||||||
|
if (!isEditing) {
|
||||||
|
if (cheat == null) {
|
||||||
|
binding.editNameInput.setText("")
|
||||||
|
binding.editNotesInput.setText("")
|
||||||
|
binding.editCodeInput.setText("")
|
||||||
|
} else {
|
||||||
|
binding.editNameInput.setText(cheat.getName())
|
||||||
|
binding.editNotesInput.setText(cheat.getNotes())
|
||||||
|
binding.editCodeInput.setText(cheat.getCode())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onIsEditingUpdated(isEditing: Boolean) {
|
||||||
|
if (isEditing) {
|
||||||
|
binding.root.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
binding.editNameInput.isEnabled = isEditing
|
||||||
|
binding.editNotesInput.isEnabled = isEditing
|
||||||
|
binding.editCodeInput.isEnabled = isEditing
|
||||||
|
|
||||||
|
binding.buttonDelete.visibility = if (isEditing) View.GONE else View.VISIBLE
|
||||||
|
binding.buttonEdit.visibility = if (isEditing) View.GONE else View.VISIBLE
|
||||||
|
binding.buttonCancel.visibility = if (isEditing) View.VISIBLE else View.GONE
|
||||||
|
binding.buttonOk.visibility = if (isEditing) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View?, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val mlpAppBar = binding.toolbarCheatDetails.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpAppBar.leftMargin = leftInsets
|
||||||
|
mlpAppBar.rightMargin = rightInsets
|
||||||
|
binding.toolbarCheatDetails.layoutParams = mlpAppBar
|
||||||
|
|
||||||
|
binding.scrollView.updatePadding(left = leftInsets, right = rightInsets)
|
||||||
|
binding.buttonContainer.updatePadding(left = leftInsets, right = rightInsets)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,71 +0,0 @@
|
||||||
package org.citra.citra_emu.features.cheats.ui;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.graphics.Insets;
|
|
||||||
import androidx.core.view.ViewCompat;
|
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
|
||||||
import org.citra.citra_emu.ui.DividerItemDecoration;
|
|
||||||
|
|
||||||
public class CheatListFragment extends Fragment {
|
|
||||||
private RecyclerView mRecyclerView;
|
|
||||||
private FloatingActionButton mFab;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
|
||||||
@Nullable Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(R.layout.fragment_cheat_list, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
|
||||||
mRecyclerView = view.findViewById(R.id.cheat_list);
|
|
||||||
mFab = view.findViewById(R.id.fab);
|
|
||||||
|
|
||||||
CheatsActivity activity = (CheatsActivity) requireActivity();
|
|
||||||
CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
|
|
||||||
|
|
||||||
mRecyclerView.setAdapter(new CheatsAdapter(activity, viewModel));
|
|
||||||
mRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
|
|
||||||
mRecyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
|
|
||||||
|
|
||||||
mFab.setOnClickListener(v -> {
|
|
||||||
viewModel.startAddingCheat();
|
|
||||||
viewModel.openDetailsView();
|
|
||||||
});
|
|
||||||
|
|
||||||
setInsets();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setInsets() {
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
|
|
||||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
|
||||||
v.setPadding(0, 0, 0, insets.bottom + getResources().getDimensionPixelSize(R.dimen.spacing_fab_list));
|
|
||||||
|
|
||||||
ViewGroup.MarginLayoutParams mlpFab =
|
|
||||||
(ViewGroup.MarginLayoutParams) mFab.getLayoutParams();
|
|
||||||
int fabPadding = getResources().getDimensionPixelSize(R.dimen.spacing_large);
|
|
||||||
mlpFab.leftMargin = insets.left + fabPadding;
|
|
||||||
mlpFab.bottomMargin = insets.bottom + fabPadding;
|
|
||||||
mlpFab.rightMargin = insets.right + fabPadding;
|
|
||||||
mFab.setLayoutParams(mlpFab);
|
|
||||||
|
|
||||||
return windowInsets;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.cheats.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.google.android.material.divider.MaterialDividerItemDecoration
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.databinding.FragmentCheatListBinding
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel
|
||||||
|
import org.citra.citra_emu.ui.main.MainActivity
|
||||||
|
|
||||||
|
class CheatListFragment : Fragment() {
|
||||||
|
private var _binding: FragmentCheatListBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val cheatsViewModel: CheatsViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentCheatListBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is using the correct scope, lint is just acting up
|
||||||
|
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.cheatList.adapter = CheatsAdapter(requireActivity(), cheatsViewModel)
|
||||||
|
binding.cheatList.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
binding.cheatList.addItemDecoration(
|
||||||
|
MaterialDividerItemDecoration(
|
||||||
|
requireContext(),
|
||||||
|
MaterialDividerItemDecoration.VERTICAL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
cheatsViewModel.cheatAddedEvent.collect { position: Int? ->
|
||||||
|
position?.let {
|
||||||
|
binding.cheatList.apply {
|
||||||
|
post { (adapter as CheatsAdapter).notifyItemInserted(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
cheatsViewModel.cheatChangedEvent.collect { position: Int? ->
|
||||||
|
position?.let {
|
||||||
|
binding.cheatList.apply {
|
||||||
|
post { (adapter as CheatsAdapter).notifyItemChanged(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
cheatsViewModel.cheatDeletedEvent.collect { position: Int? ->
|
||||||
|
position?.let {
|
||||||
|
binding.cheatList.apply {
|
||||||
|
post { (adapter as CheatsAdapter).notifyItemRemoved(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.fab.setOnClickListener {
|
||||||
|
cheatsViewModel.startAddingCheat()
|
||||||
|
cheatsViewModel.openDetailsView()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.toolbarCheatList.setNavigationOnClickListener {
|
||||||
|
if (requireActivity() is MainActivity) {
|
||||||
|
view.findNavController().popBackStack()
|
||||||
|
} else {
|
||||||
|
requireActivity().finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() {
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val mlpAppBar = binding.toolbarCheatList.layoutParams as MarginLayoutParams
|
||||||
|
mlpAppBar.leftMargin = leftInsets
|
||||||
|
mlpAppBar.rightMargin = rightInsets
|
||||||
|
binding.toolbarCheatList.layoutParams = mlpAppBar
|
||||||
|
|
||||||
|
binding.cheatList.updatePadding(
|
||||||
|
left = leftInsets,
|
||||||
|
right = rightInsets,
|
||||||
|
bottom = barInsets.bottom +
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_fab_list)
|
||||||
|
)
|
||||||
|
|
||||||
|
val mlpFab = binding.fab.layoutParams as MarginLayoutParams
|
||||||
|
val fabPadding = resources.getDimensionPixelSize(R.dimen.spacing_large)
|
||||||
|
mlpFab.leftMargin = leftInsets + fabPadding
|
||||||
|
mlpFab.bottomMargin = barInsets.bottom + fabPadding
|
||||||
|
mlpFab.rightMargin = rightInsets + fabPadding
|
||||||
|
binding.fab.layoutParams = mlpFab
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,56 +0,0 @@
|
||||||
package org.citra.citra_emu.features.cheats.ui;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.CompoundButton;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.features.cheats.model.Cheat;
|
|
||||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
|
||||||
|
|
||||||
public class CheatViewHolder extends RecyclerView.ViewHolder
|
|
||||||
implements View.OnClickListener, CompoundButton.OnCheckedChangeListener {
|
|
||||||
private final View mRoot;
|
|
||||||
private final TextView mName;
|
|
||||||
private final CheckBox mCheckbox;
|
|
||||||
|
|
||||||
private CheatsViewModel mViewModel;
|
|
||||||
private Cheat mCheat;
|
|
||||||
private int mPosition;
|
|
||||||
|
|
||||||
public CheatViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
|
|
||||||
mRoot = itemView.findViewById(R.id.root);
|
|
||||||
mName = itemView.findViewById(R.id.text_name);
|
|
||||||
mCheckbox = itemView.findViewById(R.id.checkbox);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void bind(CheatsActivity activity, Cheat cheat, int position) {
|
|
||||||
mCheckbox.setOnCheckedChangeListener(null);
|
|
||||||
|
|
||||||
mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
|
|
||||||
mCheat = cheat;
|
|
||||||
mPosition = position;
|
|
||||||
|
|
||||||
mName.setText(mCheat.getName());
|
|
||||||
mCheckbox.setChecked(mCheat.getEnabled());
|
|
||||||
|
|
||||||
mRoot.setOnClickListener(this);
|
|
||||||
mCheckbox.setOnCheckedChangeListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onClick(View root) {
|
|
||||||
mViewModel.setSelectedCheat(mCheat, mPosition);
|
|
||||||
mViewModel.openDetailsView();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
|
||||||
mCheat.setEnabled(isChecked);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,235 +0,0 @@
|
||||||
package org.citra.citra_emu.features.cheats.ui;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.core.graphics.Insets;
|
|
||||||
import androidx.core.view.ViewCompat;
|
|
||||||
import androidx.core.view.WindowCompat;
|
|
||||||
import androidx.core.view.WindowInsetsAnimationCompat;
|
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
|
|
||||||
|
|
||||||
import com.google.android.material.appbar.AppBarLayout;
|
|
||||||
import com.google.android.material.appbar.MaterialToolbar;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.features.cheats.model.Cheat;
|
|
||||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
|
||||||
import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback;
|
|
||||||
import org.citra.citra_emu.utils.InsetsHelper;
|
|
||||||
import org.citra.citra_emu.utils.ThemeUtil;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class CheatsActivity extends AppCompatActivity
|
|
||||||
implements SlidingPaneLayout.PanelSlideListener {
|
|
||||||
private static String ARG_TITLE_ID = "title_id";
|
|
||||||
|
|
||||||
private CheatsViewModel mViewModel;
|
|
||||||
|
|
||||||
private SlidingPaneLayout mSlidingPaneLayout;
|
|
||||||
private View mCheatList;
|
|
||||||
private View mCheatDetails;
|
|
||||||
|
|
||||||
private View mCheatListLastFocus;
|
|
||||||
private View mCheatDetailsLastFocus;
|
|
||||||
|
|
||||||
public static void launch(Context context, long titleId) {
|
|
||||||
Intent intent = new Intent(context, CheatsActivity.class);
|
|
||||||
intent.putExtra(ARG_TITLE_ID, titleId);
|
|
||||||
context.startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
ThemeUtil.INSTANCE.setTheme(this);
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
|
||||||
|
|
||||||
long titleId = getIntent().getLongExtra(ARG_TITLE_ID, -1);
|
|
||||||
|
|
||||||
mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class);
|
|
||||||
mViewModel.initialize(titleId);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_cheats);
|
|
||||||
|
|
||||||
mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout);
|
|
||||||
mCheatList = findViewById(R.id.cheat_list_container);
|
|
||||||
mCheatDetails = findViewById(R.id.cheat_details_container);
|
|
||||||
|
|
||||||
mCheatListLastFocus = mCheatList;
|
|
||||||
mCheatDetailsLastFocus = mCheatDetails;
|
|
||||||
|
|
||||||
mSlidingPaneLayout.addPanelSlideListener(this);
|
|
||||||
|
|
||||||
getOnBackPressedDispatcher().addCallback(this,
|
|
||||||
new TwoPaneOnBackPressedCallback(mSlidingPaneLayout));
|
|
||||||
|
|
||||||
mViewModel.getSelectedCheat().observe(this, this::onSelectedCheatChanged);
|
|
||||||
mViewModel.getIsEditing().observe(this, this::onIsEditingChanged);
|
|
||||||
onSelectedCheatChanged(mViewModel.getSelectedCheat().getValue());
|
|
||||||
|
|
||||||
mViewModel.getOpenDetailsViewEvent().observe(this, this::openDetailsView);
|
|
||||||
|
|
||||||
// Show "Up" button in the action bar for navigation
|
|
||||||
MaterialToolbar toolbar = findViewById(R.id.toolbar_cheats);
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
|
|
||||||
setInsets();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
MenuInflater inflater = getMenuInflater();
|
|
||||||
inflater.inflate(R.menu.menu_settings, menu);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
|
|
||||||
mViewModel.saveIfNeeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPanelSlide(@NonNull View panel, float slideOffset) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPanelOpened(@NonNull View panel) {
|
|
||||||
boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
|
||||||
mCheatDetailsLastFocus.requestFocus(rtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPanelClosed(@NonNull View panel) {
|
|
||||||
boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
|
||||||
mCheatListLastFocus.requestFocus(rtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onIsEditingChanged(boolean isEditing) {
|
|
||||||
if (isEditing) {
|
|
||||||
mSlidingPaneLayout.setLockMode(SlidingPaneLayout.LOCK_MODE_UNLOCKED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onSelectedCheatChanged(Cheat selectedCheat) {
|
|
||||||
boolean cheatSelected = selectedCheat != null || mViewModel.getIsEditing().getValue();
|
|
||||||
|
|
||||||
if (!cheatSelected && mSlidingPaneLayout.isOpen()) {
|
|
||||||
mSlidingPaneLayout.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
mSlidingPaneLayout.setLockMode(cheatSelected ?
|
|
||||||
SlidingPaneLayout.LOCK_MODE_UNLOCKED : SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onListViewFocusChange(boolean hasFocus) {
|
|
||||||
if (hasFocus) {
|
|
||||||
mCheatListLastFocus = mCheatList.findFocus();
|
|
||||||
if (mCheatListLastFocus == null)
|
|
||||||
throw new NullPointerException();
|
|
||||||
|
|
||||||
mSlidingPaneLayout.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onDetailsViewFocusChange(boolean hasFocus) {
|
|
||||||
if (hasFocus) {
|
|
||||||
mCheatDetailsLastFocus = mCheatDetails.findFocus();
|
|
||||||
if (mCheatDetailsLastFocus == null)
|
|
||||||
throw new NullPointerException();
|
|
||||||
|
|
||||||
mSlidingPaneLayout.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onSupportNavigateUp() {
|
|
||||||
onBackPressed();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openDetailsView(boolean open) {
|
|
||||||
if (open) {
|
|
||||||
mSlidingPaneLayout.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setOnFocusChangeListenerRecursively(@NonNull View view, View.OnFocusChangeListener listener) {
|
|
||||||
view.setOnFocusChangeListener(listener);
|
|
||||||
|
|
||||||
if (view instanceof ViewGroup) {
|
|
||||||
ViewGroup viewGroup = (ViewGroup) view;
|
|
||||||
for (int i = 0; i < viewGroup.getChildCount(); i++) {
|
|
||||||
View child = viewGroup.getChildAt(i);
|
|
||||||
setOnFocusChangeListenerRecursively(child, listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setInsets() {
|
|
||||||
AppBarLayout appBarLayout = findViewById(R.id.appbar_cheats);
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(mSlidingPaneLayout, (v, windowInsets) -> {
|
|
||||||
Insets barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
|
||||||
Insets keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime());
|
|
||||||
|
|
||||||
InsetsHelper.insetAppBar(barInsets, appBarLayout);
|
|
||||||
mSlidingPaneLayout.setPadding(barInsets.left, 0, barInsets.right, 0);
|
|
||||||
|
|
||||||
// Set keyboard insets if the system supports smooth keyboard animations
|
|
||||||
ViewGroup.MarginLayoutParams mlpDetails =
|
|
||||||
(ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams();
|
|
||||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R) {
|
|
||||||
if (keyboardInsets.bottom > 0) {
|
|
||||||
mlpDetails.bottomMargin = keyboardInsets.bottom;
|
|
||||||
} else {
|
|
||||||
mlpDetails.bottomMargin = barInsets.bottom;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (mlpDetails.bottomMargin == 0) {
|
|
||||||
mlpDetails.bottomMargin = barInsets.bottom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mCheatDetails.setLayoutParams(mlpDetails);
|
|
||||||
|
|
||||||
return windowInsets;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the layout for every frame that the keyboard animates in
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
|
||||||
ViewCompat.setWindowInsetsAnimationCallback(mCheatDetails,
|
|
||||||
new WindowInsetsAnimationCompat.Callback(
|
|
||||||
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP) {
|
|
||||||
int keyboardInsets = 0;
|
|
||||||
int barInsets = 0;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets,
|
|
||||||
@NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
|
|
||||||
ViewGroup.MarginLayoutParams mlpDetails =
|
|
||||||
(ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams();
|
|
||||||
keyboardInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
|
|
||||||
barInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
|
|
||||||
mlpDetails.bottomMargin = Math.max(keyboardInsets, barInsets);
|
|
||||||
mCheatDetails.setLayoutParams(mlpDetails);
|
|
||||||
return insets;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.cheats.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.OnFocusChangeListener
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.databinding.ActivityCheatsBinding
|
||||||
|
import org.citra.citra_emu.utils.InsetsHelper
|
||||||
|
import org.citra.citra_emu.utils.ThemeUtil
|
||||||
|
|
||||||
|
class CheatsActivity : AppCompatActivity() {
|
||||||
|
private lateinit var binding: ActivityCheatsBinding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
ThemeUtil.setTheme(this)
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
binding = ActivityCheatsBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
if (InsetsHelper.getSystemGestureType(applicationContext) !=
|
||||||
|
InsetsHelper.GESTURE_NAVIGATION
|
||||||
|
) {
|
||||||
|
binding.navigationBarShade.setBackgroundColor(
|
||||||
|
ThemeUtil.getColorWithOpacity(
|
||||||
|
MaterialColors.getColor(
|
||||||
|
binding.navigationBarShade,
|
||||||
|
com.google.android.material.R.attr.colorSurface
|
||||||
|
),
|
||||||
|
ThemeUtil.SYSTEM_BAR_ALPHA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val navHostFragment =
|
||||||
|
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
||||||
|
val navController = navHostFragment.navController
|
||||||
|
navController.setGraph(R.navigation.cheats_navigation, intent.extras)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun setOnFocusChangeListenerRecursively(view: View, listener: OnFocusChangeListener?) {
|
||||||
|
view.onFocusChangeListener = listener
|
||||||
|
if (view is ViewGroup) {
|
||||||
|
for (i in 0 until view.childCount) {
|
||||||
|
val child = view.getChildAt(i)
|
||||||
|
setOnFocusChangeListenerRecursively(child, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,72 +0,0 @@
|
||||||
package org.citra.citra_emu.features.cheats.ui;
|
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.features.cheats.model.Cheat;
|
|
||||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
|
||||||
|
|
||||||
public class CheatsAdapter extends RecyclerView.Adapter<CheatViewHolder> {
|
|
||||||
private final CheatsActivity mActivity;
|
|
||||||
private final CheatsViewModel mViewModel;
|
|
||||||
|
|
||||||
public CheatsAdapter(CheatsActivity activity, CheatsViewModel viewModel) {
|
|
||||||
mActivity = activity;
|
|
||||||
mViewModel = viewModel;
|
|
||||||
|
|
||||||
mViewModel.getCheatAddedEvent().observe(activity, (position) -> {
|
|
||||||
if (position != null) {
|
|
||||||
notifyItemInserted(position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mViewModel.getCheatUpdatedEvent().observe(activity, (position) -> {
|
|
||||||
if (position != null) {
|
|
||||||
notifyItemChanged(position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mViewModel.getCheatDeletedEvent().observe(activity, (position) -> {
|
|
||||||
if (position != null) {
|
|
||||||
notifyItemRemoved(position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public CheatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
|
||||||
|
|
||||||
View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false);
|
|
||||||
addViewListeners(cheatView);
|
|
||||||
return new CheatViewHolder(cheatView);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull CheatViewHolder holder, int position) {
|
|
||||||
holder.bind(mActivity, getItemAt(position), position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return mViewModel.getCheats().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addViewListeners(View view) {
|
|
||||||
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
|
|
||||||
// at the same time. If the user is navigating using a d-pad and moves focus to an element
|
|
||||||
// in the currently hidden pane, we need to manually show that pane.
|
|
||||||
CheatsActivity.setOnFocusChangeListenerRecursively(view,
|
|
||||||
(v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Cheat getItemAt(int position) {
|
|
||||||
return mViewModel.getCheats()[position];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.cheats.ui
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.citra.citra_emu.databinding.ListItemCheatBinding
|
||||||
|
import org.citra.citra_emu.features.cheats.model.Cheat
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel
|
||||||
|
|
||||||
|
class CheatsAdapter(
|
||||||
|
private val activity: FragmentActivity,
|
||||||
|
private val viewModel: CheatsViewModel
|
||||||
|
) : RecyclerView.Adapter<CheatsAdapter.CheatViewHolder>() {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheatViewHolder {
|
||||||
|
val binding =
|
||||||
|
ListItemCheatBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
addViewListeners(binding.root)
|
||||||
|
return CheatViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: CheatViewHolder, position: Int) =
|
||||||
|
holder.bind(activity, viewModel.cheats[position], position)
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = viewModel.cheats.size
|
||||||
|
|
||||||
|
private fun addViewListeners(view: View) {
|
||||||
|
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
|
||||||
|
// at the same time. If the user is navigating using a d-pad and moves focus to an element
|
||||||
|
// in the currently hidden pane, we need to manually show that pane.
|
||||||
|
CheatsActivity.setOnFocusChangeListenerRecursively(view) { _, hasFocus ->
|
||||||
|
viewModel.onListViewFocusChanged(hasFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class CheatViewHolder(private val binding: ListItemCheatBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root), View.OnClickListener,
|
||||||
|
CompoundButton.OnCheckedChangeListener {
|
||||||
|
private lateinit var viewModel: CheatsViewModel
|
||||||
|
private lateinit var cheat: Cheat
|
||||||
|
private var position = 0
|
||||||
|
|
||||||
|
fun bind(activity: FragmentActivity, cheat: Cheat, position: Int) {
|
||||||
|
viewModel = ViewModelProvider(activity)[CheatsViewModel::class.java]
|
||||||
|
this.cheat = cheat
|
||||||
|
this.position = position
|
||||||
|
binding.textName.text = this.cheat.getName()
|
||||||
|
binding.cheatSwitch.isChecked = this.cheat.getEnabled()
|
||||||
|
binding.cheatContainer.setOnClickListener(this)
|
||||||
|
binding.cheatSwitch.setOnCheckedChangeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(root: View) {
|
||||||
|
viewModel.setSelectedCheat(cheat, position)
|
||||||
|
viewModel.openDetailsView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||||
|
cheat.setEnabled(isChecked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,244 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.cheats.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsAnimationCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import androidx.slidingpanelayout.widget.SlidingPaneLayout
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.citra.citra_emu.databinding.FragmentCheatsBinding
|
||||||
|
import org.citra.citra_emu.features.cheats.model.Cheat
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel
|
||||||
|
import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback
|
||||||
|
import org.citra.citra_emu.ui.main.MainActivity
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
|
||||||
|
class CheatsFragment : Fragment(), SlidingPaneLayout.PanelSlideListener {
|
||||||
|
private var cheatListLastFocus: View? = null
|
||||||
|
private var cheatDetailsLastFocus: View? = null
|
||||||
|
|
||||||
|
private var _binding: FragmentCheatsBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val cheatsViewModel: CheatsViewModel by activityViewModels()
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val args by navArgs<CheatsFragmentArgs>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentCheatsBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is using the correct scope, lint is just acting up
|
||||||
|
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||||
|
|
||||||
|
cheatsViewModel.initialize(args.titleId)
|
||||||
|
|
||||||
|
cheatListLastFocus = binding.cheatListContainer
|
||||||
|
cheatDetailsLastFocus = binding.cheatDetailsContainer
|
||||||
|
binding.slidingPaneLayout.addPanelSlideListener(this)
|
||||||
|
requireActivity().onBackPressedDispatcher.addCallback(
|
||||||
|
viewLifecycleOwner,
|
||||||
|
TwoPaneOnBackPressedCallback(binding.slidingPaneLayout)
|
||||||
|
)
|
||||||
|
requireActivity().onBackPressedDispatcher.addCallback(
|
||||||
|
viewLifecycleOwner,
|
||||||
|
object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
if (binding.slidingPaneLayout.isOpen) {
|
||||||
|
binding.slidingPaneLayout.close()
|
||||||
|
} else {
|
||||||
|
if (requireActivity() is MainActivity) {
|
||||||
|
view.findNavController().popBackStack()
|
||||||
|
} else {
|
||||||
|
requireActivity().finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
cheatsViewModel.selectedCheat.collect { onSelectedCheatChanged(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
cheatsViewModel.isEditing.collect { onIsEditingChanged(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
cheatsViewModel.openDetailsViewEvent.collect { openDetailsView(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
cheatsViewModel.closeDetailsViewEvent.collect { closeDetailsView(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
cheatsViewModel.listViewFocusChange.collect { onListViewFocusChange(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
cheatsViewModel.detailsViewFocusChange.collect { onDetailsViewFocusChange(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
cheatsViewModel.saveIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPanelSlide(panel: View, slideOffset: Float) {}
|
||||||
|
override fun onPanelOpened(panel: View) {
|
||||||
|
val rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL
|
||||||
|
cheatDetailsLastFocus!!.requestFocus(if (rtl) View.FOCUS_LEFT else View.FOCUS_RIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPanelClosed(panel: View) {
|
||||||
|
val rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL
|
||||||
|
cheatListLastFocus!!.requestFocus(if (rtl) View.FOCUS_RIGHT else View.FOCUS_LEFT)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onIsEditingChanged(isEditing: Boolean) {
|
||||||
|
if (isEditing) {
|
||||||
|
binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_UNLOCKED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSelectedCheatChanged(selectedCheat: Cheat?) {
|
||||||
|
val cheatSelected = selectedCheat != null || cheatsViewModel.isEditing.value!!
|
||||||
|
if (!cheatSelected && binding.slidingPaneLayout.isOpen) {
|
||||||
|
binding.slidingPaneLayout.close()
|
||||||
|
}
|
||||||
|
binding.slidingPaneLayout.lockMode =
|
||||||
|
if (cheatSelected) SlidingPaneLayout.LOCK_MODE_UNLOCKED else SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onListViewFocusChange(hasFocus: Boolean) {
|
||||||
|
if (hasFocus) {
|
||||||
|
cheatListLastFocus = binding.cheatListContainer.findFocus()
|
||||||
|
if (cheatListLastFocus == null) throw NullPointerException()
|
||||||
|
binding.slidingPaneLayout.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDetailsViewFocusChange(hasFocus: Boolean) {
|
||||||
|
if (hasFocus) {
|
||||||
|
cheatDetailsLastFocus = binding.cheatDetailsContainer.findFocus()
|
||||||
|
if (cheatDetailsLastFocus == null) {
|
||||||
|
throw NullPointerException()
|
||||||
|
}
|
||||||
|
binding.slidingPaneLayout.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openDetailsView(open: Boolean) {
|
||||||
|
if (open) {
|
||||||
|
binding.slidingPaneLayout.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun closeDetailsView(close: Boolean) {
|
||||||
|
if (close) {
|
||||||
|
binding.slidingPaneLayout.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() {
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.slidingPaneLayout
|
||||||
|
) { _: View?, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
|
||||||
|
|
||||||
|
// Set keyboard insets if the system supports smooth keyboard animations
|
||||||
|
val mlpDetails = binding.cheatDetailsContainer.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
|
if (keyboardInsets.bottom > 0) {
|
||||||
|
mlpDetails.bottomMargin = keyboardInsets.bottom
|
||||||
|
} else {
|
||||||
|
mlpDetails.bottomMargin = barInsets.bottom
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mlpDetails.bottomMargin == 0) {
|
||||||
|
mlpDetails.bottomMargin = barInsets.bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.cheatDetailsContainer.layoutParams = mlpDetails
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the layout for every frame that the keyboard animates in
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
ViewCompat.setWindowInsetsAnimationCallback(
|
||||||
|
binding.cheatDetailsContainer,
|
||||||
|
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
|
||||||
|
var keyboardInsets = 0
|
||||||
|
var barInsets = 0
|
||||||
|
override fun onProgress(
|
||||||
|
insets: WindowInsetsCompat,
|
||||||
|
runningAnimations: List<WindowInsetsAnimationCompat>
|
||||||
|
): WindowInsetsCompat {
|
||||||
|
val mlpDetails =
|
||||||
|
binding.cheatDetailsContainer.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
keyboardInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||||
|
barInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
||||||
|
mlpDetails.bottomMargin = keyboardInsets.coerceAtLeast(barInsets)
|
||||||
|
binding.cheatDetailsContainer.layoutParams = mlpDetails
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.hotkeys
|
||||||
|
|
||||||
|
enum class Hotkey(val button: Int) {
|
||||||
|
SWAP_SCREEN(10001),
|
||||||
|
CYCLE_LAYOUT(10002),
|
||||||
|
CLOSE_GAME(10003),
|
||||||
|
PAUSE_OR_RESUME(10004);
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.hotkeys
|
||||||
|
|
||||||
|
import org.citra.citra_emu.utils.EmulationLifecycleUtil
|
||||||
|
import org.citra.citra_emu.display.ScreenAdjustmentUtil
|
||||||
|
|
||||||
|
class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil) {
|
||||||
|
|
||||||
|
val hotkeyButtons = Hotkey.entries.map { it.button }
|
||||||
|
|
||||||
|
fun handleHotkey(bindedButton: Int): Boolean {
|
||||||
|
if(hotkeyButtons.contains(bindedButton)) {
|
||||||
|
when (bindedButton) {
|
||||||
|
Hotkey.SWAP_SCREEN.button -> screenAdjustmentUtil.swapScreen()
|
||||||
|
Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts()
|
||||||
|
Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame()
|
||||||
|
Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.settings.model
|
||||||
|
|
||||||
|
interface AbstractBooleanSetting : AbstractSetting {
|
||||||
|
var boolean: Boolean
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.settings.model
|
||||||
|
|
||||||
|
interface AbstractFloatSetting : AbstractSetting {
|
||||||
|
var float: Float
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.settings.model
|
||||||
|
|
||||||
|
interface AbstractIntSetting : AbstractSetting {
|
||||||
|
var int: Int
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.settings.model
|
||||||
|
|
||||||
|
interface AbstractSetting {
|
||||||
|
val key: String?
|
||||||
|
val section: String?
|
||||||
|
val isRuntimeEditable: Boolean
|
||||||
|
val valueAsString: String
|
||||||
|
val defaultValue: Any
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.settings.model
|
||||||
|
|
||||||
|
interface AbstractShortSetting : AbstractSetting {
|
||||||
|
var short: Short
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.settings.model
|
||||||
|
|
||||||
|
interface AbstractStringSetting : AbstractSetting {
|
||||||
|
var string: String
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
package org.citra.citra_emu.features.settings.model;
|
|
||||||
|
|
||||||
public final class BooleanSetting extends Setting {
|
|
||||||
private boolean mValue;
|
|
||||||
|
|
||||||
public BooleanSetting(String key, String section, boolean value) {
|
|
||||||
super(key, section);
|
|
||||||
mValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getValue() {
|
|
||||||
return mValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setValue(boolean value) {
|
|
||||||
mValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getValueAsString() {
|
|
||||||
return mValue ? "True" : "False";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.features.settings.model
|
||||||
|
|
||||||
|
enum class BooleanSetting(
|
||||||
|
override val key: String,
|
||||||
|
override val section: String,
|
||||||
|
override val defaultValue: Boolean
|
||||||
|
) : AbstractBooleanSetting {
|
||||||
|
SPIRV_SHADER_GEN("spirv_shader_gen", Settings.SECTION_RENDERER, true),
|
||||||
|
ASYNC_SHADERS("async_shader_compilation", Settings.SECTION_RENDERER, false),
|
||||||
|
PLUGIN_LOADER("plugin_loader", Settings.SECTION_SYSTEM, false),
|
||||||
|
ALLOW_PLUGIN_LOADER("allow_plugin_loader", Settings.SECTION_SYSTEM, true),
|
||||||
|
SWAP_SCREEN("swap_screen", Settings.SECTION_LAYOUT, false);
|
||||||
|
|
||||||
|
override var boolean: Boolean = defaultValue
|
||||||
|
|
||||||
|
override val valueAsString: String
|
||||||
|
get() = boolean.toString()
|
||||||
|
|
||||||
|
override val isRuntimeEditable: Boolean
|
||||||
|
get() {
|
||||||
|
for (setting in NOT_RUNTIME_EDITABLE) {
|
||||||
|
if (setting == this) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val NOT_RUNTIME_EDITABLE = listOf(
|
||||||
|
PLUGIN_LOADER,
|
||||||
|
ALLOW_PLUGIN_LOADER
|
||||||
|
)
|
||||||
|
|
||||||
|
fun from(key: String): BooleanSetting? =
|
||||||
|
BooleanSetting.values().firstOrNull { it.key == key }
|
||||||
|
|
||||||
|
fun clear() = BooleanSetting.values().forEach { it.boolean = it.defaultValue }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
package org.citra.citra_emu.features.settings.model;
|
|
||||||
|
|
||||||
public final class FloatSetting extends Setting {
|
|
||||||
private float mValue;
|
|
||||||
|
|
||||||
public FloatSetting(String key, String section, float value) {
|
|
||||||
super(key, section);
|
|
||||||
mValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getValue() {
|
|
||||||
return mValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setValue(float value) {
|
|
||||||
mValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getValueAsString() {
|
|
||||||
return Float.toString(mValue);
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue