Compare commits

..

10 Commits

Author SHA1 Message Date
yuzubot
95c74faa10 Android 232 2024-02-05 17:04:26 +00:00
yuzubot
2ec3355348 Merge yuzu-emu#12920 2024-02-05 17:04:26 +00:00
yuzubot
8c277880d8 Merge yuzu-emu#12915 2024-02-05 17:04:26 +00:00
yuzubot
9e1e33bcca Merge yuzu-emu#12914 2024-02-05 17:04:26 +00:00
yuzubot
8e3103ceaf Merge yuzu-emu#12905 2024-02-05 17:04:26 +00:00
yuzubot
5dca9417a2 Merge yuzu-emu#12903 2024-02-05 17:04:26 +00:00
yuzubot
2cca283210 Merge yuzu-emu#12873 2024-02-05 17:04:26 +00:00
yuzubot
d90b525fc0 Merge yuzu-emu#12756 2024-02-05 17:04:26 +00:00
yuzubot
ea3e76c7f4 Merge yuzu-emu#12749 2024-02-05 17:04:26 +00:00
yuzubot
d23ae34ecc Merge yuzu-emu#12461 2024-02-05 17:04:26 +00:00
887 changed files with 39122 additions and 74223 deletions

View File

@ -1,21 +0,0 @@
#!/bin/bash -ex
# SPDX-FileCopyrightText: 2024 yuzu Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
export NDK_CCACHE="$(which ccache)"
ccache -s
export ANDROID_KEYSTORE_FILE="${GITHUB_WORKSPACE}/ks.jks"
base64 --decode <<< "${EA_PLAY_ANDROID_KEYSTORE_B64}" > "${ANDROID_KEYSTORE_FILE}"
export ANDROID_KEY_ALIAS="${PLAY_ANDROID_KEY_ALIAS}"
export ANDROID_KEYSTORE_PASS="${PLAY_ANDROID_KEYSTORE_PASS}"
export SERVICE_ACCOUNT_KEY_PATH="${GITHUB_WORKSPACE}/sa.json"
base64 --decode <<< "${EA_SERVICE_ACCOUNT_KEY_B64}" > "${SERVICE_ACCOUNT_KEY_PATH}"
./gradlew "publishEaReleaseBundle"
ccache -s
if [ ! -z "${ANDROID_KEYSTORE_B64}" ]; then
rm "${ANDROID_KEYSTORE_FILE}"
fi

View File

@ -1,21 +0,0 @@
#!/bin/bash -ex
# SPDX-FileCopyrightText: 2024 yuzu Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
export NDK_CCACHE="$(which ccache)"
ccache -s
export ANDROID_KEYSTORE_FILE="${GITHUB_WORKSPACE}/ks.jks"
base64 --decode <<< "${MAINLINE_PLAY_ANDROID_KEYSTORE_B64}" > "${ANDROID_KEYSTORE_FILE}"
export ANDROID_KEY_ALIAS="${PLAY_ANDROID_KEY_ALIAS}"
export ANDROID_KEYSTORE_PASS="${PLAY_ANDROID_KEYSTORE_PASS}"
export SERVICE_ACCOUNT_KEY_PATH="${GITHUB_WORKSPACE}/sa.json"
base64 --decode <<< "${MAINLINE_SERVICE_ACCOUNT_KEY_B64}" > "${SERVICE_ACCOUNT_KEY_PATH}"
./gradlew "publishMainlineReleaseBundle"
ccache -s
if [ ! -z "${ANDROID_KEYSTORE_B64}" ]; then
rm "${ANDROID_KEYSTORE_FILE}"
fi

View File

@ -1,66 +0,0 @@
# SPDX-FileCopyrightText: 2024 yuzu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
name: yuzu-android-ea-play-release
on:
workflow_dispatch:
inputs:
release-track:
description: 'Play store release track (internal/alpha/beta/production)'
required: true
default: 'alpha'
jobs:
android:
runs-on: ubuntu-latest
if: ${{ github.repository == 'yuzu-emu/yuzu' }}
steps:
- uses: actions/checkout@v3
name: Checkout
with:
fetch-depth: 0
submodules: true
token: ${{ secrets.ALT_GITHUB_TOKEN }}
- run: npm install execa@5
- uses: actions/github-script@v5
name: 'Merge and publish Android EA changes'
env:
ALT_GITHUB_TOKEN: ${{ secrets.ALT_GITHUB_TOKEN }}
BUILD_EA: true
with:
script: |
const execa = require("execa");
const mergebot = require('./.github/workflows/android-merge.js').mergebot;
process.chdir('${{ github.workspace }}');
mergebot(github, context, execa);
- name: Get tag name
run: echo "GIT_TAG_NAME=$(cat tag-name.txt)" >> $GITHUB_ENV
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y ccache apksigner glslang-dev glslang-tools
- name: Build
run: ./.ci/scripts/android/eabuild.sh
env:
EA_PLAY_ANDROID_KEYSTORE_B64: ${{ secrets.PLAY_ANDROID_KEYSTORE_B64 }}
PLAY_ANDROID_KEY_ALIAS: ${{ secrets.PLAY_ANDROID_KEY_ALIAS }}
PLAY_ANDROID_KEYSTORE_PASS: ${{ secrets.PLAY_ANDROID_KEYSTORE_PASS }}
EA_SERVICE_ACCOUNT_KEY_B64: ${{ secrets.EA_SERVICE_ACCOUNT_KEY_B64 }}
STORE_TRACK: ${{ github.event.inputs.release-track }}
AUTO_VERSIONED: true
BUILD_EA: true
- name: Create release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ env.EA_TAG_NAME }}
name: ${{ env.EA_TAG_NAME }}
draft: false
prerelease: false
repository: yuzu/yuzu-android
token: ${{ secrets.ALT_GITHUB_TOKEN }}

View File

@ -1,59 +0,0 @@
# SPDX-FileCopyrightText: 2024 yuzu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
name: yuzu-android-mainline-play-release
on:
workflow_dispatch:
inputs:
release-tag:
description: 'Tag # from yuzu-android that you want to build and publish'
required: true
default: '200'
release-track:
description: 'Play store release track (internal/alpha/beta/production)'
required: true
default: 'alpha'
jobs:
android:
runs-on: ubuntu-latest
if: ${{ github.repository == 'yuzu-emu/yuzu' }}
steps:
- uses: actions/checkout@v3
name: Checkout
with:
fetch-depth: 0
submodules: true
token: ${{ secrets.ALT_GITHUB_TOKEN }}
- run: npm install execa@5
- uses: actions/github-script@v5
name: 'Pull mainline tag'
env:
MAINLINE_TAG: ${{ github.event.inputs.release-tag }}
with:
script: |
const execa = require("execa");
const mergebot = require('./.github/workflows/android-merge.js').getMainlineTag;
process.chdir('${{ github.workspace }}');
mergebot(execa);
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y ccache apksigner glslang-dev glslang-tools
- name: Build
run: |
echo "GIT_TAG_NAME=android-${{ github.event.inputs.releast-tag }}" >> $GITHUB_ENV
./.ci/scripts/android/mainlinebuild.sh
env:
MAINLINE_PLAY_ANDROID_KEYSTORE_B64: ${{ secrets.PLAY_ANDROID_KEYSTORE_B64 }}
PLAY_ANDROID_KEY_ALIAS: ${{ secrets.PLAY_ANDROID_KEY_ALIAS }}
PLAY_ANDROID_KEYSTORE_PASS: ${{ secrets.PLAY_ANDROID_KEYSTORE_PASS }}
SERVICE_ACCOUNT_KEY_B64: ${{ secrets.MAINLINE_SERVICE_ACCOUNT_KEY_B64 }}
STORE_TRACK: ${{ github.event.inputs.release-track }}
AUTO_VERSIONED: true

View File

@ -6,12 +6,9 @@
const fs = require("fs"); const fs = require("fs");
// which label to check for changes // which label to check for changes
const CHANGE_LABEL_MAINLINE = 'android-merge'; const CHANGE_LABEL = 'android-merge';
const CHANGE_LABEL_EA = 'android-ea-merge';
// how far back in time should we consider the changes are "recent"? (default: 24 hours) // how far back in time should we consider the changes are "recent"? (default: 24 hours)
const DETECTION_TIME_FRAME = (parseInt(process.env.DETECTION_TIME_FRAME)) || (24 * 3600 * 1000); const DETECTION_TIME_FRAME = (parseInt(process.env.DETECTION_TIME_FRAME)) || (24 * 3600 * 1000);
const BUILD_EA = process.env.BUILD_EA == 'true';
const MAINLINE_TAG = process.env.MAINLINE_TAG;
async function checkBaseChanges(github) { async function checkBaseChanges(github) {
// query the commit date of the latest commit on this branch // query the commit date of the latest commit on this branch
@ -43,7 +40,20 @@ async function checkBaseChanges(github) {
async function checkAndroidChanges(github) { async function checkAndroidChanges(github) {
if (checkBaseChanges(github)) return true; if (checkBaseChanges(github)) return true;
const pulls = getPulls(github, false); const query = `query($owner:String!, $name:String!, $label:String!) {
repository(name:$name, owner:$owner) {
pullRequests(labels: [$label], states: OPEN, first: 100) {
nodes { number headRepository { pushedAt } }
}
}
}`;
const variables = {
owner: 'yuzu-emu',
name: 'yuzu',
label: CHANGE_LABEL,
};
const result = await github.graphql(query, variables);
const pulls = result.repository.pullRequests.nodes;
for (let i = 0; i < pulls.length; i++) { for (let i = 0; i < pulls.length; i++) {
let pull = pulls[i]; let pull = pulls[i];
if (new Date() - new Date(pull.headRepository.pushedAt) <= DETECTION_TIME_FRAME) { if (new Date() - new Date(pull.headRepository.pushedAt) <= DETECTION_TIME_FRAME) {
@ -73,13 +83,7 @@ async function tagAndPush(github, owner, repo, execa, commit=false) {
}; };
const tags = await github.graphql(query, variables); const tags = await github.graphql(query, variables);
const tagList = tags.repository.refs.nodes; const tagList = tags.repository.refs.nodes;
let lastTag = 'android-1'; const lastTag = tagList[0] ? tagList[0].name : 'dummy-0';
for (let i = 0; i < tagList.length; ++i) {
if (tagList[i].name.includes('android-')) {
lastTag = tagList[i].name;
break;
}
}
const tagNumber = /\w+-(\d+)/.exec(lastTag)[1] | 0; const tagNumber = /\w+-(\d+)/.exec(lastTag)[1] | 0;
const channel = repo.split('-')[1]; const channel = repo.split('-')[1];
const newTag = `${channel}-${tagNumber + 1}`; const newTag = `${channel}-${tagNumber + 1}`;
@ -97,48 +101,6 @@ async function tagAndPush(github, owner, repo, execa, commit=false) {
console.info('Successfully pushed new changes.'); console.info('Successfully pushed new changes.');
} }
async function tagAndPushEA(github, owner, repo, execa) {
let altToken = process.env.ALT_GITHUB_TOKEN;
if (!altToken) {
throw `Please set ALT_GITHUB_TOKEN environment variable. This token should have write access to ${owner}/${repo}.`;
}
const query = `query ($owner:String!, $name:String!) {
repository(name:$name, owner:$owner) {
refs(refPrefix: "refs/tags/", orderBy: {field: TAG_COMMIT_DATE, direction: DESC}, first: 10) {
nodes { name }
}
}
}`;
const variables = {
owner: owner,
name: repo,
};
const tags = await github.graphql(query, variables);
const tagList = tags.repository.refs.nodes;
let lastTag = 'ea-1';
for (let i = 0; i < tagList.length; ++i) {
if (tagList[i].name.includes('ea-')) {
lastTag = tagList[i].name;
break;
}
}
const tagNumber = /\w+-(\d+)/.exec(lastTag)[1] | 0;
const newTag = `ea-${tagNumber + 1}`;
console.log(`New tag: ${newTag}`);
console.info('Pushing tags to GitHub ...');
await execa("git", ["remote", "add", "android", "https://github.com/yuzu-emu/yuzu-android.git"]);
await execa("git", ["fetch", "android"]);
await execa("git", ['tag', newTag]);
await execa("git", ['push', 'android', `${newTag}`]);
fs.writeFile('tag-name.txt', newTag, (err) => {
if (err) throw 'Could not write tag name to file!'
})
console.info('Successfully pushed new changes.');
}
async function generateReadme(pulls, context, mergeResults, execa) { async function generateReadme(pulls, context, mergeResults, execa) {
let baseUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/`; let baseUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/`;
let output = let output =
@ -240,7 +202,10 @@ async function resetBranch(execa) {
} }
} }
async function getPulls(github) { async function mergebot(github, context, execa) {
// Reset our local copy of master to what appears on yuzu-emu/yuzu - master
await resetBranch(execa);
const query = `query ($owner:String!, $name:String!, $label:String!) { const query = `query ($owner:String!, $name:String!, $label:String!) {
repository(name:$name, owner:$owner) { repository(name:$name, owner:$owner) {
pullRequests(labels: [$label], states: OPEN, first: 100) { pullRequests(labels: [$label], states: OPEN, first: 100) {
@ -250,49 +215,13 @@ async function getPulls(github) {
} }
} }
}`; }`;
const mainlineVariables = { const variables = {
owner: 'yuzu-emu', owner: 'yuzu-emu',
name: 'yuzu', name: 'yuzu',
label: CHANGE_LABEL_MAINLINE, label: CHANGE_LABEL,
}; };
const mainlineResult = await github.graphql(query, mainlineVariables); const result = await github.graphql(query, variables);
const pulls = mainlineResult.repository.pullRequests.nodes; const pulls = result.repository.pullRequests.nodes;
if (BUILD_EA) {
const eaVariables = {
owner: 'yuzu-emu',
name: 'yuzu',
label: CHANGE_LABEL_EA,
};
const eaResult = await github.graphql(query, eaVariables);
const eaPulls = eaResult.repository.pullRequests.nodes;
return pulls.concat(eaPulls);
}
return pulls;
}
async function getMainlineTag(execa) {
console.log(`::group::Getting mainline tag android-${MAINLINE_TAG}`);
let hasFailed = false;
try {
await execa("git", ["remote", "add", "mainline", "https://github.com/yuzu-emu/yuzu-android.git"]);
await execa("git", ["fetch", "mainline", "--tags"]);
await execa("git", ["checkout", `tags/android-${MAINLINE_TAG}`]);
await execa("git", ["submodule", "update", "--init", "--recursive"]);
} catch (err) {
console.log('::error title=Failed pull tag');
hasFailed = true;
}
console.log("::endgroup::");
if (hasFailed) {
throw 'Failed pull mainline tag. Aborting!';
}
}
async function mergebot(github, context, execa) {
// Reset our local copy of master to what appears on yuzu-emu/yuzu - master
await resetBranch(execa);
const pulls = await getPulls(github);
let displayList = []; let displayList = [];
for (let i = 0; i < pulls.length; i++) { for (let i = 0; i < pulls.length; i++) {
let pull = pulls[i]; let pull = pulls[i];
@ -302,17 +231,11 @@ async function mergebot(github, context, execa) {
console.table(displayList); console.table(displayList);
await fetchPullRequests(pulls, "https://github.com/yuzu-emu/yuzu", execa); await fetchPullRequests(pulls, "https://github.com/yuzu-emu/yuzu", execa);
const mergeResults = await mergePullRequests(pulls, execa); const mergeResults = await mergePullRequests(pulls, execa);
await generateReadme(pulls, context, mergeResults, execa);
if (BUILD_EA) { await tagAndPush(github, 'yuzu-emu', `yuzu-android`, execa, true);
await tagAndPushEA(github, 'yuzu-emu', `yuzu-android`, execa);
} else {
await generateReadme(pulls, context, mergeResults, execa);
await tagAndPush(github, 'yuzu-emu', `yuzu-android`, execa, true);
}
} }
module.exports.mergebot = mergebot; module.exports.mergebot = mergebot;
module.exports.checkAndroidChanges = checkAndroidChanges; module.exports.checkAndroidChanges = checkAndroidChanges;
module.exports.tagAndPush = tagAndPush; module.exports.tagAndPush = tagAndPush;
module.exports.checkBaseChanges = checkBaseChanges; module.exports.checkBaseChanges = checkBaseChanges;
module.exports.getMainlineTag = getMainlineTag;

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2024 yuzu Emulator Project # SPDX-FileCopyrightText: 2023 yuzu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
name: yuzu-android-publish name: yuzu-android-publish
@ -16,7 +16,7 @@ on:
jobs: jobs:
android: android:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event.inputs.android != 'false' && github.repository == 'yuzu-emu/yuzu' }} if: ${{ github.event.inputs.android != 'false' && github.repository == 'yuzu-emu/yuzu-android' }}
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@v3

View File

@ -81,7 +81,8 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Install dependencies - name: Install dependencies
run: | run: |
brew install autoconf automake boost ccache ffmpeg fmt glslang hidapi libtool libusb lz4 ninja nlohmann-json openssl pkg-config qt@5 sdl2 speexdsp zlib zlib zstd # workaround for https://github.com/actions/setup-python/issues/577
brew install autoconf automake boost@1.83 ccache ffmpeg fmt glslang hidapi libtool libusb lz4 ninja nlohmann-json openssl pkg-config qt@5 sdl2 speexdsp zlib zlib zstd || brew link --overwrite python@3.12
- name: Build - name: Build
run: | run: |
mkdir build mkdir build

View File

@ -1,9 +1,14 @@
| Pull Request | Commit | Title | Author | Merged? | | Pull Request | Commit | Title | Author | Merged? |
|----|----|----|----|----| |----|----|----|----|----|
| [12461](https://github.com/yuzu-emu/yuzu//pull/12461) | [`7464cae24`](https://github.com/yuzu-emu/yuzu//pull/12461/files) | Rework Nvdec and VIC to fix out-of-order videos, and speed up decoding. | [Kelebek1](https://github.com/Kelebek1/) | Yes | | [12461](https://github.com/yuzu-emu/yuzu-android//pull/12461) | [`4c08a0e6d`](https://github.com/yuzu-emu/yuzu-android//pull/12461/files) | Rework Nvdec and VIC to fix out-of-order videos, and speed up decoding. | [Kelebek1](https://github.com/Kelebek1/) | Yes |
| [13018](https://github.com/yuzu-emu/yuzu//pull/13018) | [`01cbc638a`](https://github.com/yuzu-emu/yuzu//pull/13018/files) | am: rewrite part 2 | [liamwhite](https://github.com/liamwhite/) | Yes | | [12749](https://github.com/yuzu-emu/yuzu-android//pull/12749) | [`aad4b0d6f`](https://github.com/yuzu-emu/yuzu-android//pull/12749/files) | general: workarounds for SMMU syncing issues | [liamwhite](https://github.com/liamwhite/) | Yes |
| [13174](https://github.com/yuzu-emu/yuzu//pull/13174) | [`7d1284826`](https://github.com/yuzu-emu/yuzu//pull/13174/files) | glue/time: Remove global variables | [FearlessTobi](https://github.com/FearlessTobi/) | Yes | | [12756](https://github.com/yuzu-emu/yuzu-android//pull/12756) | [`cd3de0848`](https://github.com/yuzu-emu/yuzu-android//pull/12756/files) | general: applet multiprocess | [liamwhite](https://github.com/liamwhite/) | Yes |
| [13177](https://github.com/yuzu-emu/yuzu//pull/13177) | [`f5cc94f05`](https://github.com/yuzu-emu/yuzu//pull/13177/files) | vfs: misc performance improvements | [liamwhite](https://github.com/liamwhite/) | Yes | | [12873](https://github.com/yuzu-emu/yuzu-android//pull/12873) | [`023c3aa65`](https://github.com/yuzu-emu/yuzu-android//pull/12873/files) | GPU: Implement channel scheduling. | [FernandoS27](https://github.com/FernandoS27/) | Yes |
| [12903](https://github.com/yuzu-emu/yuzu-android//pull/12903) | [`f296a9ce9`](https://github.com/yuzu-emu/yuzu-android//pull/12903/files) | shader_recompiler: use only ConstOffset for OpImageFetch | [liamwhite](https://github.com/liamwhite/) | Yes |
| [12905](https://github.com/yuzu-emu/yuzu-android//pull/12905) | [`5eb5c9675`](https://github.com/yuzu-emu/yuzu-android//pull/12905/files) | nvnflinger: release buffers before presentation sleep | [liamwhite](https://github.com/liamwhite/) | Yes |
| [12914](https://github.com/yuzu-emu/yuzu-android//pull/12914) | [`3a6d8ae2c`](https://github.com/yuzu-emu/yuzu-android//pull/12914/files) | VideoCore Refactor Part 1. | [FernandoS27](https://github.com/FernandoS27/) | Yes |
| [12915](https://github.com/yuzu-emu/yuzu-android//pull/12915) | [`504abbd6e`](https://github.com/yuzu-emu/yuzu-android//pull/12915/files) | dmnt: cheats: Update cheat vm to latest version | [german77](https://github.com/german77/) | Yes |
| [12920](https://github.com/yuzu-emu/yuzu-android//pull/12920) | [`62fc6d5c3`](https://github.com/yuzu-emu/yuzu-android//pull/12920/files) | android: Move JNI setup and helpers to common | [t895](https://github.com/t895/) | Yes |
End of merge log. You can find the original README.md below the break. End of merge log. You can find the original README.md below the break.

View File

@ -11,4 +11,3 @@ type = QT
file_filter = ../../src/android/app/src/main/res/values-<lang>/strings.xml file_filter = ../../src/android/app/src/main/res/values-<lang>/strings.xml
source_file = ../../src/android/app/src/main/res/values/strings.xml source_file = ../../src/android/app/src/main/res/values/strings.xml
type = ANDROID type = ANDROID
lang_map = ja_JP:ja, ko_KR:ko, pt_BR:pt-rBR, pt_PT:pt-rPT, ru_RU:ru, vi_VN:vi, zh_CN:zh-rCN, zh_TW:zh-rTW

1626
dist/languages/ar.ts vendored

File diff suppressed because it is too large Load Diff

1628
dist/languages/ca.ts vendored

File diff suppressed because it is too large Load Diff

1628
dist/languages/cs.ts vendored

File diff suppressed because it is too large Load Diff

1628
dist/languages/da.ts vendored

File diff suppressed because it is too large Load Diff

1636
dist/languages/de.ts vendored

File diff suppressed because it is too large Load Diff

1644
dist/languages/el.ts vendored

File diff suppressed because it is too large Load Diff

1720
dist/languages/es.ts vendored

File diff suppressed because it is too large Load Diff

10
dist/languages/fi.ts vendored
View File

@ -4684,10 +4684,10 @@ Please, only use this feature to install updates and DLC.</source>
</message> </message>
<message> <message>
<location filename="../../src/yuzu/main.cpp" line="3386"/> <location filename="../../src/yuzu/main.cpp" line="3386"/>
<source>You are about to force rederive all of your keys. <source>You are about to force rederive all of your keys.
If you do not know what this means or what you are doing, If you do not know what this means or what you are doing,
this is a potentially destructive action. this is a potentially destructive action.
Please make sure this is what you want Please make sure this is what you want
and optionally make backups. and optionally make backups.
This will delete your autogenerated key files and re-run the key derivation module.</source> This will delete your autogenerated key files and re-run the key derivation module.</source>
@ -4731,7 +4731,7 @@ Tämä poistaa automaattisesti generoidut avaimet ja ajaa avainten laskentamoduu
<message> <message>
<location filename="../../src/yuzu/main.cpp" line="3441"/> <location filename="../../src/yuzu/main.cpp" line="3441"/>
<source>Deriving keys... <source>Deriving keys...
This may take up to a minute depending This may take up to a minute depending
on your system&apos;s performance.</source> on your system&apos;s performance.</source>
<translation>Johdetaan avaimia... <translation>Johdetaan avaimia...
Tähän voi kulua jonkin aikaa Tähän voi kulua jonkin aikaa

1686
dist/languages/fr.ts vendored

File diff suppressed because it is too large Load Diff

1770
dist/languages/hu.ts vendored

File diff suppressed because it is too large Load Diff

2105
dist/languages/id.ts vendored

File diff suppressed because it is too large Load Diff

1640
dist/languages/it.ts vendored

File diff suppressed because it is too large Load Diff

1634
dist/languages/ja_JP.ts vendored

File diff suppressed because it is too large Load Diff

1632
dist/languages/ko_KR.ts vendored

File diff suppressed because it is too large Load Diff

1630
dist/languages/nb.ts vendored

File diff suppressed because it is too large Load Diff

1630
dist/languages/nl.ts vendored

File diff suppressed because it is too large Load Diff

1634
dist/languages/pl.ts vendored

File diff suppressed because it is too large Load Diff

2028
dist/languages/pt_BR.ts vendored

File diff suppressed because it is too large Load Diff

1682
dist/languages/pt_PT.ts vendored

File diff suppressed because it is too large Load Diff

1675
dist/languages/ru_RU.ts vendored

File diff suppressed because it is too large Load Diff

1630
dist/languages/sv.ts vendored

File diff suppressed because it is too large Load Diff

1630
dist/languages/tr_TR.ts vendored

File diff suppressed because it is too large Load Diff

1630
dist/languages/uk.ts vendored

File diff suppressed because it is too large Load Diff

1630
dist/languages/vi.ts vendored

File diff suppressed because it is too large Load Diff

1630
dist/languages/vi_VN.ts vendored

File diff suppressed because it is too large Load Diff

1717
dist/languages/zh_CN.ts vendored

File diff suppressed because it is too large Load Diff

1682
dist/languages/zh_TW.ts vendored

File diff suppressed because it is too large Load Diff

View File

@ -314,10 +314,3 @@ endif()
if (NOT TARGET SimpleIni::SimpleIni) if (NOT TARGET SimpleIni::SimpleIni)
add_subdirectory(simpleini) add_subdirectory(simpleini)
endif() endif()
# sse2neon
if (ARCHITECTURE_arm64 AND NOT TARGET sse2neon)
add_library(sse2neon INTERFACE)
target_include_directories(sse2neon INTERFACE sse2neon)
endif()

2
externals/dynarmic vendored

@ -1 +1 @@
Subproject commit ba8192d89078af51ae6f97c9352e3683612cdff1 Subproject commit ca0e264f4f962e29baa23a3282ce484625866b98

@ -1 +1 @@
Subproject commit 97929690234f2b4add36b33657fe3fe09bd57dfd Subproject commit 404d39004570a26c734a9d1fa29ab4d63089c599

File diff suppressed because it is too large Load Diff

View File

@ -121,7 +121,6 @@ else()
-Wno-attributes -Wno-attributes
-Wno-invalid-offsetof -Wno-invalid-offsetof
-Wno-unused-parameter -Wno-unused-parameter
-Wno-missing-field-initializers
) )
if (CMAKE_CXX_COMPILER_ID MATCHES Clang) # Clang or AppleClang if (CMAKE_CXX_COMPILER_ID MATCHES Clang) # Clang or AppleClang

View File

@ -3,8 +3,8 @@
import android.annotation.SuppressLint import android.annotation.SuppressLint
import kotlin.collections.setOf import kotlin.collections.setOf
import org.jetbrains.kotlin.konan.properties.Properties
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
import com.github.triplet.gradle.androidpublisher.ReleaseStatus
plugins { plugins {
id("com.android.application") id("com.android.application")
@ -13,7 +13,6 @@ plugins {
kotlin("plugin.serialization") version "1.9.20" kotlin("plugin.serialization") version "1.9.20"
id("androidx.navigation.safeargs.kotlin") id("androidx.navigation.safeargs.kotlin")
id("org.jlleitschuh.gradle.ktlint") version "11.4.0" id("org.jlleitschuh.gradle.ktlint") version "11.4.0"
id("com.github.triplet.play") version "3.8.6"
} }
/** /**
@ -59,7 +58,15 @@ android {
targetSdk = 34 targetSdk = 34
versionName = getGitVersion() versionName = getGitVersion()
versionCode = if (System.getenv("AUTO_VERSIONED") == "true") { // If you want to use autoVersion for the versionCode, create a property in local.properties
// named "autoVersioned" and set it to "true"
val properties = Properties()
val versionProperty = try {
properties.load(project.rootProject.file("local.properties").inputStream())
properties.getProperty("autoVersioned") ?: ""
} catch (e: Exception) { "" }
versionCode = if (versionProperty == "true") {
autoVersion autoVersion
} else { } else {
1 1
@ -214,15 +221,6 @@ ktlint {
} }
} }
play {
val keyPath = System.getenv("SERVICE_ACCOUNT_KEY_PATH")
if (keyPath != null) {
serviceAccountCredentials.set(File(keyPath))
}
track.set(System.getenv("STORE_TRACK") ?: "internal")
releaseStatus.set(ReleaseStatus.COMPLETED)
}
dependencies { dependencies {
implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.appcompat:appcompat:1.6.1")
@ -259,18 +257,12 @@ fun runGitCommand(command: List<String>): String {
} }
fun getGitVersion(): String { fun getGitVersion(): String {
val gitVersion = runGitCommand(
listOf(
"git",
"describe",
"--always",
"--long"
)
).replace(Regex("(-0)?-[^-]+$"), "")
val versionName = if (System.getenv("GITHUB_ACTIONS") != null) { val versionName = if (System.getenv("GITHUB_ACTIONS") != null) {
System.getenv("GIT_TAG_NAME") ?: gitVersion val gitTag = System.getenv("GIT_TAG_NAME") ?: ""
gitTag
} else { } else {
gitVersion runGitCommand(listOf("git", "describe", "--always", "--long"))
.replace(Regex("(-0)?-[^-]+$"), "")
} }
return versionName.ifEmpty { "0.0" } return versionName.ifEmpty { "0.0" }
} }

View File

@ -12,9 +12,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" /> <uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<application <application
android:name="org.yuzu.yuzu_emu.YuzuApplication" android:name="org.yuzu.yuzu_emu.YuzuApplication"
@ -79,6 +80,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
android:resource="@xml/nfc_tech_filter" /> android:resource="@xml/nfc_tech_filter" />
</activity> </activity>
<service android:name="org.yuzu.yuzu_emu.utils.ForegroundService" android:foregroundServiceType="specialUse">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Keep emulation running in background"/>
</service>
<provider <provider
android:name=".features.DocumentProvider" android:name=".features.DocumentProvider"
android:authorities="${applicationId}.user" android:authorities="${applicationId}.user"

View File

@ -3,21 +3,24 @@
package org.yuzu.yuzu_emu package org.yuzu.yuzu_emu
import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.text.Html import android.text.Html
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.Surface import android.view.Surface
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.fragments.CoreErrorDialogFragment
import org.yuzu.yuzu_emu.utils.DocumentsTree import org.yuzu.yuzu_emu.utils.DocumentsTree
import org.yuzu.yuzu_emu.utils.FileUtil import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
import org.yuzu.yuzu_emu.model.InstallResult import org.yuzu.yuzu_emu.model.InstallResult
import org.yuzu.yuzu_emu.model.Patch import org.yuzu.yuzu_emu.model.Patch
import org.yuzu.yuzu_emu.model.GameVerificationResult import org.yuzu.yuzu_emu.model.GameVerificationResult
@ -27,6 +30,34 @@ import org.yuzu.yuzu_emu.model.GameVerificationResult
* with the native side of the Yuzu code. * with the native side of the Yuzu code.
*/ */
object NativeLibrary { object NativeLibrary {
/**
* Default controller id for each device
*/
const val Player1Device = 0
const val Player2Device = 1
const val Player3Device = 2
const val Player4Device = 3
const val Player5Device = 4
const val Player6Device = 5
const val Player7Device = 6
const val Player8Device = 7
const val ConsoleDevice = 8
/**
* Controller type for each device
*/
const val ProController = 3
const val Handheld = 4
const val JoyconDual = 5
const val JoyconLeft = 6
const val JoyconRight = 7
const val GameCube = 8
const val Pokeball = 9
const val NES = 10
const val SNES = 11
const val N64 = 12
const val SegaGenesis = 13
@JvmField @JvmField
var sEmulationActivity = WeakReference<EmulationActivity?>(null) var sEmulationActivity = WeakReference<EmulationActivity?>(null)
@ -96,6 +127,112 @@ object NativeLibrary {
FileUtil.getFilename(Uri.parse(path)) FileUtil.getFilename(Uri.parse(path))
} }
/**
* Returns true if pro controller isn't available and handheld is
*/
external fun isHandheldOnly(): Boolean
/**
* Changes controller type for a specific device.
*
* @param Device The input descriptor of the gamepad.
* @param Type The NpadStyleIndex of the gamepad.
*/
external fun setDeviceType(Device: Int, Type: Int): Boolean
/**
* Handles event when a gamepad is connected.
*
* @param Device The input descriptor of the gamepad.
*/
external fun onGamePadConnectEvent(Device: Int): Boolean
/**
* Handles event when a gamepad is disconnected.
*
* @param Device The input descriptor of the gamepad.
*/
external fun onGamePadDisconnectEvent(Device: Int): Boolean
/**
* Handles button press events for a gamepad.
*
* @param Device The input descriptor of the gamepad.
* @param Button Key code identifying which button was pressed.
* @param Action Mask identifying which action is happening (button pressed down, or button released).
* @return If we handled the button press.
*/
external fun onGamePadButtonEvent(Device: Int, Button: Int, Action: Int): Boolean
/**
* Handles joystick movement events.
*
* @param Device The device ID of the gamepad.
* @param Axis The axis ID
* @param x_axis The value of the x-axis represented by the given ID.
* @param y_axis The value of the y-axis represented by the given ID.
*/
external fun onGamePadJoystickEvent(
Device: Int,
Axis: Int,
x_axis: Float,
y_axis: Float
): Boolean
/**
* Handles motion events.
*
* @param delta_timestamp The finger id corresponding to this event
* @param gyro_x,gyro_y,gyro_z The value of the accelerometer sensor.
* @param accel_x,accel_y,accel_z The value of the y-axis
*/
external fun onGamePadMotionEvent(
Device: Int,
delta_timestamp: Long,
gyro_x: Float,
gyro_y: Float,
gyro_z: Float,
accel_x: Float,
accel_y: Float,
accel_z: Float
): Boolean
/**
* Signals and load a nfc tag
*
* @param data Byte array containing all the data from a nfc tag
*/
external fun onReadNfcTag(data: ByteArray?): Boolean
/**
* Removes current loaded nfc tag
*/
external fun onRemoveNfcTag(): Boolean
/**
* Handles touch press events.
*
* @param finger_id The finger id corresponding to this event
* @param x_axis The value of the x-axis.
* @param y_axis The value of the y-axis.
*/
external fun onTouchPressed(finger_id: Int, x_axis: Float, y_axis: Float)
/**
* Handles touch movement.
*
* @param x_axis The value of the instantaneous x-axis.
* @param y_axis The value of the instantaneous y-axis.
*/
external fun onTouchMoved(finger_id: Int, x_axis: Float, y_axis: Float)
/**
* Handles touch release events.
*
* @param finger_id The finger id corresponding to this event
*/
external fun onTouchReleased(finger_id: Int)
external fun setAppDirectory(directory: String) external fun setAppDirectory(directory: String)
/** /**
@ -181,13 +318,46 @@ object NativeLibrary {
ErrorUnknown ErrorUnknown
} }
var coreErrorAlertResult = false private var coreErrorAlertResult = false
val coreErrorAlertLock = Object() private val coreErrorAlertLock = Object()
class CoreErrorDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val title = requireArguments().serializable<String>("title")
val message = requireArguments().serializable<String>("message")
return MaterialAlertDialogBuilder(requireActivity())
.setTitle(title)
.setMessage(message)
.setPositiveButton(R.string.continue_button, null)
.setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
coreErrorAlertResult = false
synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
}
.create()
}
override fun onDismiss(dialog: DialogInterface) {
coreErrorAlertResult = true
synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
}
companion object {
fun newInstance(title: String?, message: String?): CoreErrorDialogFragment {
val frag = CoreErrorDialogFragment()
val args = Bundle()
args.putString("title", title)
args.putString("message", message)
frag.arguments = args
return frag
}
}
}
private fun onCoreErrorImpl(title: String, message: String) { private fun onCoreErrorImpl(title: String, message: String) {
val emulationActivity = sEmulationActivity.get() val emulationActivity = sEmulationActivity.get()
if (emulationActivity == null) { if (emulationActivity == null) {
Log.error("[NativeLibrary] EmulationActivity not present") error("[NativeLibrary] EmulationActivity not present")
return return
} }
@ -203,7 +373,7 @@ object NativeLibrary {
fun onCoreError(error: CoreError?, details: String): Boolean { fun onCoreError(error: CoreError?, details: String): Boolean {
val emulationActivity = sEmulationActivity.get() val emulationActivity = sEmulationActivity.get()
if (emulationActivity == null) { if (emulationActivity == null) {
Log.error("[NativeLibrary] EmulationActivity not present") error("[NativeLibrary] EmulationActivity not present")
return false return false
} }
@ -234,7 +404,7 @@ object NativeLibrary {
} }
// Show the AlertDialog on the main thread. // Show the AlertDialog on the main thread.
emulationActivity.runOnUiThread { onCoreErrorImpl(title, message) } emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) })
// Wait for the lock to notify that it is complete. // Wait for the lock to notify that it is complete.
synchronized(coreErrorAlertLock) { coreErrorAlertLock.wait() } synchronized(coreErrorAlertLock) { coreErrorAlertLock.wait() }
@ -459,4 +629,46 @@ object NativeLibrary {
* Checks if all necessary keys are present for decryption * Checks if all necessary keys are present for decryption
*/ */
external fun areKeysPresent(): Boolean external fun areKeysPresent(): Boolean
/**
* Button type for use in onTouchEvent
*/
object ButtonType {
const val BUTTON_A = 0
const val BUTTON_B = 1
const val BUTTON_X = 2
const val BUTTON_Y = 3
const val STICK_L = 4
const val STICK_R = 5
const val TRIGGER_L = 6
const val TRIGGER_R = 7
const val TRIGGER_ZL = 8
const val TRIGGER_ZR = 9
const val BUTTON_PLUS = 10
const val BUTTON_MINUS = 11
const val DPAD_LEFT = 12
const val DPAD_UP = 13
const val DPAD_RIGHT = 14
const val DPAD_DOWN = 15
const val BUTTON_SL = 16
const val BUTTON_SR = 17
const val BUTTON_HOME = 18
const val BUTTON_CAPTURE = 19
}
/**
* Stick type for use in onTouchEvent
*/
object StickType {
const val STICK_L = 0
const val STICK_R = 1
}
/**
* Button states
*/
object ButtonState {
const val RELEASED = 0
const val PRESSED = 1
}
} }

View File

@ -7,7 +7,6 @@ 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 org.yuzu.yuzu_emu.features.input.NativeInput
import java.io.File import java.io.File
import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.DocumentsTree import org.yuzu.yuzu_emu.utils.DocumentsTree
@ -18,6 +17,17 @@ fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir
class YuzuApplication : Application() { class YuzuApplication : Application() {
private fun createNotificationChannels() { private fun createNotificationChannels() {
val emulationChannel = NotificationChannel(
getString(R.string.emulation_notification_channel_id),
getString(R.string.emulation_notification_channel_name),
NotificationManager.IMPORTANCE_LOW
)
emulationChannel.description = getString(
R.string.emulation_notification_channel_description
)
emulationChannel.setSound(null, null)
emulationChannel.vibrationPattern = null
val noticeChannel = NotificationChannel( val noticeChannel = NotificationChannel(
getString(R.string.notice_notification_channel_id), getString(R.string.notice_notification_channel_id),
getString(R.string.notice_notification_channel_name), getString(R.string.notice_notification_channel_name),
@ -29,6 +39,7 @@ class YuzuApplication : Application() {
// Register the channel with the system; you can't change the importance // Register the channel with the system; you can't change the importance
// or other notification behaviors after this // or other notification behaviors after this
val notificationManager = getSystemService(NotificationManager::class.java) val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(emulationChannel)
notificationManager.createNotificationChannel(noticeChannel) notificationManager.createNotificationChannel(noticeChannel)
} }
@ -38,7 +49,6 @@ class YuzuApplication : Application() {
documentsTree = DocumentsTree() documentsTree = DocumentsTree()
DirectoryInitialization.start() DirectoryInitialization.start()
GpuDriverHelper.initializeDriverParameters() GpuDriverHelper.initializeDriverParameters()
NativeInput.reloadInputDevices()
NativeLibrary.logDeviceInfo() NativeLibrary.logDeviceInfo()
Log.logDeviceInfo() Log.logDeviceInfo()

View File

@ -4,6 +4,7 @@
package org.yuzu.yuzu_emu.activities package org.yuzu.yuzu_emu.activities
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
import android.app.RemoteAction import android.app.RemoteAction
@ -39,18 +40,16 @@ import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.model.EmulationViewModel import org.yuzu.yuzu_emu.model.EmulationViewModel
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.ForegroundService
import org.yuzu.yuzu_emu.utils.InputHandler import org.yuzu.yuzu_emu.utils.InputHandler
import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.MemoryUtil import org.yuzu.yuzu_emu.utils.MemoryUtil
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.NfcReader import org.yuzu.yuzu_emu.utils.NfcReader
import org.yuzu.yuzu_emu.utils.ParamPackage
import org.yuzu.yuzu_emu.utils.ThemeHelper import org.yuzu.yuzu_emu.utils.ThemeHelper
import java.text.NumberFormat import java.text.NumberFormat
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -66,6 +65,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private var motionTimestamp: Long = 0 private var motionTimestamp: Long = 0
private var flipMotionOrientation: Boolean = false private var flipMotionOrientation: Boolean = false
private var controllerIds = InputHandler.getGameControllerIds()
private val actionPause = "ACTION_EMULATOR_PAUSE" private val actionPause = "ACTION_EMULATOR_PAUSE"
private val actionPlay = "ACTION_EMULATOR_PLAY" private val actionPlay = "ACTION_EMULATOR_PLAY"
private val actionMute = "ACTION_EMULATOR_MUTE" private val actionMute = "ACTION_EMULATOR_MUTE"
@ -73,39 +74,17 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private val emulationViewModel: EmulationViewModel by viewModels() private val emulationViewModel: EmulationViewModel by viewModels()
override fun onDestroy() {
stopForegroundService(this)
super.onDestroy()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Log.gameLaunched = true Log.gameLaunched = true
ThemeHelper.setTheme(this) ThemeHelper.setTheme(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
InputHandler.updateControllerData()
val players = NativeConfig.getInputSettings(true)
var hasConfiguredControllers = false
players.forEach {
if (it.hasMapping()) {
hasConfiguredControllers = true
}
}
if (!hasConfiguredControllers && InputHandler.androidControllers.isNotEmpty()) {
var params: ParamPackage? = null
for (controller in InputHandler.registeredControllers) {
if (controller.get("port", -1) == 0) {
params = controller
break
}
}
if (params != null) {
NativeInput.updateMappingsWithDefault(
0,
params,
params.get("display", getString(R.string.unknown))
)
NativeConfig.saveGlobalConfig()
}
}
binding = ActivityEmulationBinding.inflate(layoutInflater) binding = ActivityEmulationBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@ -123,6 +102,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
nfcReader = NfcReader(this) nfcReader = NfcReader(this)
nfcReader.initialize() nfcReader.initialize()
InputHandler.initialize()
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) { if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) {
if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) { if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) {
@ -144,6 +125,10 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
.apply() .apply()
} }
} }
// Start a foreground service to prevent the app from getting killed in the background
val startIntent = Intent(this, ForegroundService::class.java)
startForegroundService(startIntent)
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
@ -173,7 +158,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
super.onResume() super.onResume()
nfcReader.startScanning() nfcReader.startScanning()
startMotionSensorListener() startMotionSensorListener()
InputHandler.updateControllerData() InputHandler.updateControllerIds()
buildPictureInPictureParams() buildPictureInPictureParams()
} }
@ -198,7 +183,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
super.onNewIntent(intent) super.onNewIntent(intent)
setIntent(intent) setIntent(intent)
nfcReader.onNewIntent(intent) nfcReader.onNewIntent(intent)
InputHandler.updateControllerData()
} }
override fun dispatchKeyEvent(event: KeyEvent): Boolean { override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@ -271,8 +255,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
} }
val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000 val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000
motionTimestamp = event.timestamp motionTimestamp = event.timestamp
NativeInput.onDeviceMotionEvent( NativeLibrary.onGamePadMotionEvent(
NativeInput.Player1Device, NativeLibrary.Player1Device,
deltaTimestamp, deltaTimestamp,
gyro[0], gyro[0],
gyro[1], gyro[1],
@ -281,8 +265,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
accel[1], accel[1],
accel[2] accel[2]
) )
NativeInput.onDeviceMotionEvent( NativeLibrary.onGamePadMotionEvent(
NativeInput.ConsoleDevice, NativeLibrary.ConsoleDevice,
deltaTimestamp, deltaTimestamp,
gyro[0], gyro[0],
gyro[1], gyro[1],
@ -497,6 +481,12 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
activity.startActivity(launcher) activity.startActivity(launcher)
} }
fun stopForegroundService(activity: Activity) {
val startIntent = Intent(activity, ForegroundService::class.java)
startIntent.action = ForegroundService.ACTION_STOP
activity.startForegroundService(startIntent)
}
private fun areCoordinatesOutside(view: View?, x: Float, y: Float): Boolean { private fun areCoordinatesOutside(view: View?, x: Float, y: Float): Boolean {
if (view == null) { if (view == null) {
return true return true

View File

@ -3,15 +3,15 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding
import org.yuzu.yuzu_emu.features.settings.model.StringSetting import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.model.Driver import org.yuzu.yuzu_emu.model.Driver
import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class DriverAdapter(private val driverViewModel: DriverViewModel) : class DriverAdapter(private val driverViewModel: DriverViewModel) :
@ -44,15 +44,25 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :
} }
// Delay marquee by 3s // Delay marquee by 3s
title.marquee() title.postDelayed(
version.marquee() {
description.marquee() title.isSelected = true
title.ellipsize = TextUtils.TruncateAt.MARQUEE
version.isSelected = true
version.ellipsize = TextUtils.TruncateAt.MARQUEE
description.isSelected = true
description.ellipsize = TextUtils.TruncateAt.MARQUEE
},
3000
)
title.text = model.title title.text = model.title
version.text = model.version version.text = model.version
description.text = model.description description.text = model.description
buttonDelete.setVisible( if (model.title != binding.root.context.getString(R.string.system_gpu_driver)) {
model.title != binding.root.context.getString(R.string.system_gpu_driver) buttonDelete.visibility = View.VISIBLE
) } else {
buttonDelete.visibility = View.GONE
}
} }
} }
} }

View File

@ -4,6 +4,7 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.net.Uri import android.net.Uri
import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
@ -11,7 +12,6 @@ import org.yuzu.yuzu_emu.databinding.CardFolderBinding
import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
import org.yuzu.yuzu_emu.model.GameDir import org.yuzu.yuzu_emu.model.GameDir
import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) : class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
@ -29,7 +29,13 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
override fun bind(model: GameDir) { override fun bind(model: GameDir) {
binding.apply { binding.apply {
path.text = Uri.parse(model.uriString).path path.text = Uri.parse(model.uriString).path
path.marquee() path.postDelayed(
{
path.isSelected = true
path.ellipsize = TextUtils.TruncateAt.MARQUEE
},
3000
)
buttonEdit.setOnClickListener { buttonEdit.setOnClickListener {
GameFolderPropertiesDialogFragment.newInstance(model) GameFolderPropertiesDialogFragment.newInstance(model)

View File

@ -4,6 +4,7 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.net.Uri import android.net.Uri
import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
@ -26,7 +27,6 @@ import org.yuzu.yuzu_emu.databinding.CardGameBinding
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.utils.GameIconUtils import org.yuzu.yuzu_emu.utils.GameIconUtils
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class GameAdapter(private val activity: AppCompatActivity) : class GameAdapter(private val activity: AppCompatActivity) :
@ -44,7 +44,14 @@ class GameAdapter(private val activity: AppCompatActivity) :
binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ") binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ")
binding.textGameTitle.marquee() binding.textGameTitle.postDelayed(
{
binding.textGameTitle.ellipsize = TextUtils.TruncateAt.MARQUEE
binding.textGameTitle.isSelected = true
},
3000
)
binding.cardGame.setOnClickListener { onClick(model) } binding.cardGame.setOnClickListener { onClick(model) }
binding.cardGame.setOnLongClickListener { onLongClick(model) } binding.cardGame.setOnLongClickListener { onLongClick(model) }
} }

View File

@ -3,18 +3,21 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding
import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
import org.yuzu.yuzu_emu.model.GameProperty import org.yuzu.yuzu_emu.model.GameProperty
import org.yuzu.yuzu_emu.model.InstallableProperty import org.yuzu.yuzu_emu.model.InstallableProperty
import org.yuzu.yuzu_emu.model.SubmenuProperty import org.yuzu.yuzu_emu.model.SubmenuProperty
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class GamePropertiesAdapter( class GamePropertiesAdapter(
@ -73,15 +76,23 @@ class GamePropertiesAdapter(
) )
) )
binding.details.marquee() binding.details.postDelayed({
binding.details.isSelected = true
binding.details.ellipsize = TextUtils.TruncateAt.MARQUEE
}, 3000)
if (submenuProperty.details != null) { if (submenuProperty.details != null) {
binding.details.setVisible(true) binding.details.visibility = View.VISIBLE
binding.details.text = submenuProperty.details.invoke() binding.details.text = submenuProperty.details.invoke()
} else if (submenuProperty.detailsFlow != null) { } else if (submenuProperty.detailsFlow != null) {
binding.details.setVisible(true) binding.details.visibility = View.VISIBLE
submenuProperty.detailsFlow.collect(viewLifecycle) { binding.details.text = it } viewLifecycle.lifecycleScope.launch {
viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
submenuProperty.detailsFlow.collect { binding.details.text = it }
}
}
} else { } else {
binding.details.setVisible(false) binding.details.visibility = View.GONE
} }
} }
} }
@ -101,10 +112,14 @@ class GamePropertiesAdapter(
) )
) )
binding.buttonInstall.setVisible(installableProperty.install != null) if (installableProperty.install != null) {
binding.buttonInstall.setOnClickListener { installableProperty.install?.invoke() } binding.buttonInstall.visibility = View.VISIBLE
binding.buttonExport.setVisible(installableProperty.export != null) binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() }
binding.buttonExport.setOnClickListener { installableProperty.export?.invoke() } }
if (installableProperty.export != null) {
binding.buttonExport.visibility = View.VISIBLE
binding.buttonExport.setOnClickListener { installableProperty.export.invoke() }
}
} }
} }

View File

@ -3,19 +3,22 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.model.HomeSetting import org.yuzu.yuzu_emu.model.HomeSetting
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class HomeSettingAdapter( class HomeSettingAdapter(
@ -56,8 +59,18 @@ class HomeSettingAdapter(
binding.optionIcon.alpha = 0.5f binding.optionIcon.alpha = 0.5f
} }
model.details.collect(viewLifecycle) { updateOptionDetails(it) } viewLifecycle.lifecycleScope.launch {
binding.optionDetail.marquee() viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
model.details.collect { updateOptionDetails(it) }
}
}
binding.optionDetail.postDelayed(
{
binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE
binding.optionDetail.isSelected = true
},
3000
)
binding.root.setOnClickListener { onClick(model) } binding.root.setOnClickListener { onClick(model) }
} }
@ -77,7 +90,7 @@ class HomeSettingAdapter(
private fun updateOptionDetails(detailString: String) { private fun updateOptionDetails(detailString: String) {
if (detailString.isNotEmpty()) { if (detailString.isNotEmpty()) {
binding.optionDetail.text = detailString binding.optionDetail.text = detailString
binding.optionDetail.setVisible(true) binding.optionDetail.visibility = View.VISIBLE
} }
} }
} }

View File

@ -4,10 +4,10 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import org.yuzu.yuzu_emu.databinding.CardInstallableBinding import org.yuzu.yuzu_emu.databinding.CardInstallableBinding
import org.yuzu.yuzu_emu.model.Installable import org.yuzu.yuzu_emu.model.Installable
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class InstallableAdapter(installables: List<Installable>) : class InstallableAdapter(installables: List<Installable>) :
@ -26,10 +26,14 @@ class InstallableAdapter(installables: List<Installable>) :
binding.title.setText(model.titleId) binding.title.setText(model.titleId)
binding.description.setText(model.descriptionId) binding.description.setText(model.descriptionId)
binding.buttonInstall.setVisible(model.install != null) if (model.install != null) {
binding.buttonInstall.setOnClickListener { model.install?.invoke() } binding.buttonInstall.visibility = View.VISIBLE
binding.buttonExport.setVisible(model.export != null) binding.buttonInstall.setOnClickListener { model.install.invoke() }
binding.buttonExport.setOnClickListener { model.export?.invoke() } }
if (model.export != null) {
binding.buttonExport.visibility = View.VISIBLE
binding.buttonExport.setOnClickListener { model.export.invoke() }
}
} }
} }
} }

View File

@ -4,12 +4,12 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment
import org.yuzu.yuzu_emu.model.License import org.yuzu.yuzu_emu.model.License
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) : class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) :
@ -25,7 +25,7 @@ class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<Lic
binding.apply { binding.apply {
textSettingName.text = root.context.getString(model.titleId) textSettingName.text = root.context.getString(model.titleId)
textSettingDescription.text = root.context.getString(model.descriptionId) textSettingDescription.text = root.context.getString(model.descriptionId)
textSettingValue.setVisible(false) textSettingValue.visibility = View.GONE
root.setOnClickListener { onClick(model) } root.setOnClickListener { onClick(model) }
} }

View File

@ -5,6 +5,7 @@ package org.yuzu.yuzu_emu.adapters
import android.text.Html import android.text.Html
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
@ -16,7 +17,6 @@ import org.yuzu.yuzu_emu.model.SetupCallback
import org.yuzu.yuzu_emu.model.SetupPage import org.yuzu.yuzu_emu.model.SetupPage
import org.yuzu.yuzu_emu.model.StepState import org.yuzu.yuzu_emu.model.StepState
import org.yuzu.yuzu_emu.utils.ViewUtils import org.yuzu.yuzu_emu.utils.ViewUtils
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) : class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
@ -30,8 +30,8 @@ class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
AbstractViewHolder<SetupPage>(binding), SetupCallback { AbstractViewHolder<SetupPage>(binding), SetupCallback {
override fun bind(model: SetupPage) { override fun bind(model: SetupPage) {
if (model.stepCompleted.invoke() == StepState.COMPLETE) { if (model.stepCompleted.invoke() == StepState.COMPLETE) {
binding.buttonAction.setVisible(visible = false, gone = false) binding.buttonAction.visibility = View.INVISIBLE
binding.textConfirmation.setVisible(true) binding.textConfirmation.visibility = View.VISIBLE
} }
binding.icon.setImageDrawable( binding.icon.setImageDrawable(

View File

@ -1,416 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input
import org.yuzu.yuzu_emu.features.input.model.NativeButton
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
import org.yuzu.yuzu_emu.features.input.model.InputType
import org.yuzu.yuzu_emu.features.input.model.ButtonName
import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ParamPackage
import android.view.InputDevice
object NativeInput {
/**
* Default controller id for each device
*/
const val Player1Device = 0
const val Player2Device = 1
const val Player3Device = 2
const val Player4Device = 3
const val Player5Device = 4
const val Player6Device = 5
const val Player7Device = 6
const val Player8Device = 7
const val ConsoleDevice = 8
/**
* Button states
*/
object ButtonState {
const val RELEASED = 0
const val PRESSED = 1
}
/**
* Returns true if pro controller isn't available and handheld is.
* Intended to check where the input overlay should direct its inputs.
*/
external fun isHandheldOnly(): Boolean
/**
* Handles button press events for a gamepad.
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param port Port determined by controller connection order.
* @param buttonId The Android Keycode corresponding to this event.
* @param action Mask identifying which action is happening (button pressed down, or button released).
*/
external fun onGamePadButtonEvent(
guid: String,
port: Int,
buttonId: Int,
action: Int
)
/**
* Handles axis movement events.
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param port Port determined by controller connection order.
* @param axis The axis ID.
* @param value Value along the given axis.
*/
external fun onGamePadAxisEvent(guid: String, port: Int, axis: Int, value: Float)
/**
* Handles motion events.
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param port Port determined by controller connection order.
* @param deltaTimestamp The finger id corresponding to this event.
* @param xGyro The value of the x-axis for the gyroscope.
* @param yGyro The value of the y-axis for the gyroscope.
* @param zGyro The value of the z-axis for the gyroscope.
* @param xAccel The value of the x-axis for the accelerometer.
* @param yAccel The value of the y-axis for the accelerometer.
* @param zAccel The value of the z-axis for the accelerometer.
*/
external fun onGamePadMotionEvent(
guid: String,
port: Int,
deltaTimestamp: Long,
xGyro: Float,
yGyro: Float,
zGyro: Float,
xAccel: Float,
yAccel: Float,
zAccel: Float
)
/**
* Signals and load a nfc tag
* @param data Byte array containing all the data from a nfc tag.
*/
external fun onReadNfcTag(data: ByteArray?)
/**
* Removes current loaded nfc tag.
*/
external fun onRemoveNfcTag()
/**
* Handles touch press events.
* @param fingerId The finger id corresponding to this event.
* @param xAxis The value of the x-axis on the touchscreen.
* @param yAxis The value of the y-axis on the touchscreen.
*/
external fun onTouchPressed(fingerId: Int, xAxis: Float, yAxis: Float)
/**
* Handles touch movement.
* @param fingerId The finger id corresponding to this event.
* @param xAxis The value of the x-axis on the touchscreen.
* @param yAxis The value of the y-axis on the touchscreen.
*/
external fun onTouchMoved(fingerId: Int, xAxis: Float, yAxis: Float)
/**
* Handles touch release events.
* @param fingerId The finger id corresponding to this event
*/
external fun onTouchReleased(fingerId: Int)
/**
* Sends a button input to the global virtual controllers.
* @param port Port determined by controller connection order.
* @param button The [NativeButton] corresponding to this event.
* @param action Mask identifying which action is happening (button pressed down, or button released).
*/
fun onOverlayButtonEvent(port: Int, button: NativeButton, action: Int) =
onOverlayButtonEventImpl(port, button.int, action)
private external fun onOverlayButtonEventImpl(port: Int, buttonId: Int, action: Int)
/**
* Sends a joystick input to the global virtual controllers.
* @param port Port determined by controller connection order.
* @param stick The [NativeAnalog] corresponding to this event.
* @param xAxis Value along the X axis.
* @param yAxis Value along the Y axis.
*/
fun onOverlayJoystickEvent(port: Int, stick: NativeAnalog, xAxis: Float, yAxis: Float) =
onOverlayJoystickEventImpl(port, stick.int, xAxis, yAxis)
private external fun onOverlayJoystickEventImpl(
port: Int,
stickId: Int,
xAxis: Float,
yAxis: Float
)
/**
* Handles motion events for the global virtual controllers.
* @param port Port determined by controller connection order
* @param deltaTimestamp The finger id corresponding to this event.
* @param xGyro The value of the x-axis for the gyroscope.
* @param yGyro The value of the y-axis for the gyroscope.
* @param zGyro The value of the z-axis for the gyroscope.
* @param xAccel The value of the x-axis for the accelerometer.
* @param yAccel The value of the y-axis for the accelerometer.
* @param zAccel The value of the z-axis for the accelerometer.
*/
external fun onDeviceMotionEvent(
port: Int,
deltaTimestamp: Long,
xGyro: Float,
yGyro: Float,
zGyro: Float,
xAccel: Float,
yAccel: Float,
zAccel: Float
)
/**
* Reloads all input devices from the currently loaded Settings::values.players into HID Core
*/
external fun reloadInputDevices()
/**
* Registers a controller to be used with mapping
* @param device An [InputDevice] or the input overlay wrapped with [YuzuInputDevice]
*/
external fun registerController(device: YuzuInputDevice)
/**
* Gets the names of input devices that have been registered with the input subsystem via [registerController]
*/
external fun getInputDevices(): Array<String>
/**
* Reads all input profiles from disk. Must be called before creating a profile picker.
*/
external fun loadInputProfiles()
/**
* Gets the names of each available input profile.
*/
external fun getInputProfileNames(): Array<String>
/**
* Checks if the user-provided name for an input profile is valid.
* @param name User-provided name for an input profile.
* @return Whether [name] is valid or not.
*/
external fun isProfileNameValid(name: String): Boolean
/**
* Creates a new input profile.
* @param name The new profile's name.
* @param playerIndex Index of the player that's currently being edited. Used to write the profile
* name to this player's config.
* @return Whether creating the profile was successful or not.
*/
external fun createProfile(name: String, playerIndex: Int): Boolean
/**
* Deletes an input profile.
* @param name Name of the profile to delete.
* @param playerIndex Index of the player that's currently being edited. Used to remove the profile
* name from this player's config if they have it loaded.
* @return Whether deleting this profile was successful or not.
*/
external fun deleteProfile(name: String, playerIndex: Int): Boolean
/**
* Loads an input profile.
* @param name Name of the input profile to load.
* @param playerIndex Index of the player that will have this profile loaded.
* @return Whether loading this profile was successful or not.
*/
external fun loadProfile(name: String, playerIndex: Int): Boolean
/**
* Saves an input profile.
* @param name Name of the profile to save.
* @param playerIndex Index of the player that's currently being edited. Used to write the profile
* name to this player's config.
* @return Whether saving the profile was successful or not.
*/
external fun saveProfile(name: String, playerIndex: Int): Boolean
/**
* Intended to be used immediately before a call to [NativeConfig.saveControlPlayerValues]
* Must be used while per-game config is loaded.
*/
external fun loadPerGameConfiguration(
playerIndex: Int,
selectedIndex: Int,
selectedProfileName: String
)
/**
* Tells the input subsystem to start listening for inputs to map.
* @param type Type of input to map as shown by the int property in each [InputType].
*/
external fun beginMapping(type: Int)
/**
* Gets an input's [ParamPackage] as a serialized string. Used for input verification before mapping.
* Must be run after [beginMapping] and before [stopMapping].
*/
external fun getNextInput(): String
/**
* Tells the input subsystem to stop listening for inputs to map.
*/
external fun stopMapping()
/**
* Updates a controller's mappings with auto-mapping params.
* @param playerIndex Index of the player to auto-map.
* @param deviceParams [ParamPackage] representing the device to auto-map as received
* from [getInputDevices].
* @param displayName Name of the device to auto-map as received from the "display" param in [deviceParams].
* Intended to be a way to provide a default name for a controller if the "display" param is empty.
*/
fun updateMappingsWithDefault(
playerIndex: Int,
deviceParams: ParamPackage,
displayName: String
) = updateMappingsWithDefaultImpl(playerIndex, deviceParams.serialize(), displayName)
private external fun updateMappingsWithDefaultImpl(
playerIndex: Int,
deviceParams: String,
displayName: String
)
/**
* Gets the params for a specific button.
* @param playerIndex Index of the player to get params from.
* @param button The [NativeButton] to get params for.
* @return A [ParamPackage] representing a player's specific button.
*/
fun getButtonParam(playerIndex: Int, button: NativeButton): ParamPackage =
ParamPackage(getButtonParamImpl(playerIndex, button.int))
private external fun getButtonParamImpl(playerIndex: Int, buttonId: Int): String
/**
* Sets the params for a specific button.
* @param playerIndex Index of the player to set params for.
* @param button The [NativeButton] to set params for.
* @param param A [ParamPackage] to set.
*/
fun setButtonParam(playerIndex: Int, button: NativeButton, param: ParamPackage) =
setButtonParamImpl(playerIndex, button.int, param.serialize())
private external fun setButtonParamImpl(playerIndex: Int, buttonId: Int, param: String)
/**
* Gets the params for a specific stick.
* @param playerIndex Index of the player to get params from.
* @param stick The [NativeAnalog] to get params for.
* @return A [ParamPackage] representing a player's specific stick.
*/
fun getStickParam(playerIndex: Int, stick: NativeAnalog): ParamPackage =
ParamPackage(getStickParamImpl(playerIndex, stick.int))
private external fun getStickParamImpl(playerIndex: Int, stickId: Int): String
/**
* Sets the params for a specific stick.
* @param playerIndex Index of the player to set params for.
* @param stick The [NativeAnalog] to set params for.
* @param param A [ParamPackage] to set.
*/
fun setStickParam(playerIndex: Int, stick: NativeAnalog, param: ParamPackage) =
setStickParamImpl(playerIndex, stick.int, param.serialize())
private external fun setStickParamImpl(playerIndex: Int, stickId: Int, param: String)
/**
* Gets the int representation of a [ButtonName]. Tells you what to show as the mapped input for
* a button/analog/other.
* @param param A [ParamPackage] that represents a specific button's params.
* @return The [ButtonName] for [param].
*/
fun getButtonName(param: ParamPackage): ButtonName =
ButtonName.from(getButtonNameImpl(param.serialize()))
private external fun getButtonNameImpl(param: String): Int
/**
* Gets each supported [NpadStyleIndex] for a given player.
* @param playerIndex Index of the player to get supported indexes for.
* @return List of each supported [NpadStyleIndex].
*/
fun getSupportedStyleTags(playerIndex: Int): List<NpadStyleIndex> =
getSupportedStyleTagsImpl(playerIndex).map { NpadStyleIndex.from(it) }
private external fun getSupportedStyleTagsImpl(playerIndex: Int): IntArray
/**
* Gets the [NpadStyleIndex] for a given player.
* @param playerIndex Index of the player to get an [NpadStyleIndex] from.
* @return The [NpadStyleIndex] for a given player.
*/
fun getStyleIndex(playerIndex: Int): NpadStyleIndex =
NpadStyleIndex.from(getStyleIndexImpl(playerIndex))
private external fun getStyleIndexImpl(playerIndex: Int): Int
/**
* Sets the [NpadStyleIndex] for a given player.
* @param playerIndex Index of the player to change.
* @param style The new style to set.
*/
fun setStyleIndex(playerIndex: Int, style: NpadStyleIndex) =
setStyleIndexImpl(playerIndex, style.int)
private external fun setStyleIndexImpl(playerIndex: Int, styleIndex: Int)
/**
* Checks if a device is a controller.
* @param params [ParamPackage] for an input device retrieved from [getInputDevices]
* @return Whether the device is a controller or not.
*/
fun isController(params: ParamPackage): Boolean = isControllerImpl(params.serialize())
private external fun isControllerImpl(params: String): Boolean
/**
* Checks if a controller is connected
* @param playerIndex Index of the player to check.
* @return Whether the player is connected or not.
*/
external fun getIsConnected(playerIndex: Int): Boolean
/**
* Connects/disconnects a controller and ensures that connection order stays in-tact.
* @param playerIndex Index of the player to connect/disconnect.
* @param connected Whether to connect or disconnect this controller.
*/
fun connectControllers(playerIndex: Int, connected: Boolean = true) {
val connectedControllers = mutableListOf<Boolean>().apply {
if (connected) {
for (i in 0 until 8) {
add(i <= playerIndex)
}
} else {
for (i in 0 until 8) {
add(i < playerIndex)
}
}
}
connectControllersImpl(connectedControllers.toBooleanArray())
}
private external fun connectControllersImpl(connected: BooleanArray)
/**
* Resets all of the button and analog mappings for a player.
* @param playerIndex Index of the player that will have its mappings reset.
*/
external fun resetControllerMappings(playerIndex: Int)
}

View File

@ -1,93 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input
import android.view.InputDevice
import androidx.annotation.Keep
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.utils.InputHandler.getGUID
@Keep
interface YuzuInputDevice {
fun getName(): String
fun getGUID(): String
fun getPort(): Int
fun getSupportsVibration(): Boolean
fun vibrate(intensity: Float)
fun getAxes(): Array<Int> = arrayOf()
fun hasKeys(keys: IntArray): BooleanArray = BooleanArray(0)
}
class YuzuPhysicalDevice(
private val device: InputDevice,
private val port: Int,
useSystemVibrator: Boolean
) : YuzuInputDevice {
private val vibrator = if (useSystemVibrator) {
YuzuVibrator.getSystemVibrator()
} else {
YuzuVibrator.getControllerVibrator(device)
}
override fun getName(): String {
return device.name
}
override fun getGUID(): String {
return device.getGUID()
}
override fun getPort(): Int {
return port
}
override fun getSupportsVibration(): Boolean {
return vibrator.supportsVibration()
}
override fun vibrate(intensity: Float) {
vibrator.vibrate(intensity)
}
override fun getAxes(): Array<Int> = device.motionRanges.map { it.axis }.toTypedArray()
override fun hasKeys(keys: IntArray): BooleanArray = device.hasKeys(*keys)
}
class YuzuInputOverlayDevice(
private val vibration: Boolean,
private val port: Int
) : YuzuInputDevice {
private val vibrator = YuzuVibrator.getSystemVibrator()
override fun getName(): String {
return YuzuApplication.appContext.getString(R.string.input_overlay)
}
override fun getGUID(): String {
return "00000000000000000000000000000000"
}
override fun getPort(): Int {
return port
}
override fun getSupportsVibration(): Boolean {
if (vibration) {
return vibrator.supportsVibration()
}
return false
}
override fun vibrate(intensity: Float) {
if (vibration) {
vibrator.vibrate(intensity)
}
}
}

View File

@ -1,76 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input
import android.content.Context
import android.os.Build
import android.os.CombinedVibration
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.view.InputDevice
import androidx.annotation.Keep
import androidx.annotation.RequiresApi
import org.yuzu.yuzu_emu.YuzuApplication
@Keep
@Suppress("DEPRECATION")
interface YuzuVibrator {
fun supportsVibration(): Boolean
fun vibrate(intensity: Float)
companion object {
fun getControllerVibrator(device: InputDevice): YuzuVibrator =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
YuzuVibratorManager(device.vibratorManager)
} else {
YuzuVibratorManagerCompat(device.vibrator)
}
fun getSystemVibrator(): YuzuVibrator =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = YuzuApplication.appContext
.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
YuzuVibratorManager(vibratorManager)
} else {
val vibrator = YuzuApplication.appContext
.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
YuzuVibratorManagerCompat(vibrator)
}
fun getVibrationEffect(intensity: Float): VibrationEffect? {
if (intensity > 0f) {
return VibrationEffect.createOneShot(
50,
(255.0 * intensity).toInt().coerceIn(1, 255)
)
}
return null
}
}
}
@RequiresApi(Build.VERSION_CODES.S)
class YuzuVibratorManager(private val vibratorManager: VibratorManager) : YuzuVibrator {
override fun supportsVibration(): Boolean {
return vibratorManager.vibratorIds.isNotEmpty()
}
override fun vibrate(intensity: Float) {
val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return
vibratorManager.vibrate(CombinedVibration.createParallel(vibration))
}
}
class YuzuVibratorManagerCompat(private val vibrator: Vibrator) : YuzuVibrator {
override fun supportsVibration(): Boolean {
return vibrator.hasVibrator()
}
override fun vibrate(intensity: Float) {
val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return
vibrator.vibrate(vibration)
}
}

View File

@ -1,11 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
enum class AnalogDirection(val int: Int, val param: String) {
Up(0, "up"),
Down(1, "down"),
Left(2, "left"),
Right(3, "right")
}

View File

@ -1,19 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
// Loosely matches the enum in common/input.h
enum class ButtonName(val int: Int) {
Invalid(1),
// This will display the engine name instead of the button name
Engine(2),
// This will display the button by value instead of the button name
Value(3);
companion object {
fun from(int: Int): ButtonName = entries.firstOrNull { it.int == int } ?: Invalid
}
}

View File

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
// Must match the corresponding enum in input_common/main.h
enum class InputType(val int: Int) {
None(0),
Button(1),
Stick(2),
Motion(3),
Touch(4)
}

View File

@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
// Must match enum in src/common/settings_input.h
enum class NativeAnalog(val int: Int) {
LStick(0),
RStick(1);
companion object {
fun from(int: Int): NativeAnalog = entries.firstOrNull { it.int == int } ?: LStick
}
}

View File

@ -1,38 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
// Must match enum in src/common/settings_input.h
enum class NativeButton(val int: Int) {
A(0),
B(1),
X(2),
Y(3),
LStick(4),
RStick(5),
L(6),
R(7),
ZL(8),
ZR(9),
Plus(10),
Minus(11),
DLeft(12),
DUp(13),
DRight(14),
DDown(15),
SLLeft(16),
SRLeft(17),
Home(18),
Capture(19),
SLRight(20),
SRRight(21);
companion object {
fun from(int: Int): NativeButton = entries.firstOrNull { it.int == int } ?: A
}
}

View File

@ -1,10 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
// Must match enum in src/common/settings_input.h
enum class NativeTrigger(val int: Int) {
LTrigger(0),
RTrigger(1)
}

View File

@ -1,30 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.R
// Must match enum in src/core/hid/hid_types.h
enum class NpadStyleIndex(val int: Int, @StringRes val nameId: Int = 0) {
None(0),
Fullkey(3, R.string.pro_controller),
Handheld(4, R.string.handheld),
HandheldNES(4),
JoyconDual(5, R.string.dual_joycons),
JoyconLeft(6, R.string.left_joycon),
JoyconRight(7, R.string.right_joycon),
GameCube(8, R.string.gamecube_controller),
Pokeball(9),
NES(10),
SNES(12),
N64(13),
SegaGenesis(14),
SystemExt(32),
System(33);
companion object {
fun from(int: Int): NpadStyleIndex = entries.firstOrNull { it.int == int } ?: None
}
}

View File

@ -1,83 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
import androidx.annotation.Keep
@Keep
data class PlayerInput(
var connected: Boolean,
var buttons: Array<String>,
var analogs: Array<String>,
var motions: Array<String>,
var vibrationEnabled: Boolean,
var vibrationStrength: Int,
var bodyColorLeft: Long,
var bodyColorRight: Long,
var buttonColorLeft: Long,
var buttonColorRight: Long,
var profileName: String,
var useSystemVibrator: Boolean
) {
// It's recommended to use the generated equals() and hashCode() methods
// when using arrays in a data class
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PlayerInput
if (connected != other.connected) return false
if (!buttons.contentEquals(other.buttons)) return false
if (!analogs.contentEquals(other.analogs)) return false
if (!motions.contentEquals(other.motions)) return false
if (vibrationEnabled != other.vibrationEnabled) return false
if (vibrationStrength != other.vibrationStrength) return false
if (bodyColorLeft != other.bodyColorLeft) return false
if (bodyColorRight != other.bodyColorRight) return false
if (buttonColorLeft != other.buttonColorLeft) return false
if (buttonColorRight != other.buttonColorRight) return false
if (profileName != other.profileName) return false
return useSystemVibrator == other.useSystemVibrator
}
override fun hashCode(): Int {
var result = connected.hashCode()
result = 31 * result + buttons.contentHashCode()
result = 31 * result + analogs.contentHashCode()
result = 31 * result + motions.contentHashCode()
result = 31 * result + vibrationEnabled.hashCode()
result = 31 * result + vibrationStrength
result = 31 * result + bodyColorLeft.hashCode()
result = 31 * result + bodyColorRight.hashCode()
result = 31 * result + buttonColorLeft.hashCode()
result = 31 * result + buttonColorRight.hashCode()
result = 31 * result + profileName.hashCode()
result = 31 * result + useSystemVibrator.hashCode()
return result
}
fun hasMapping(): Boolean {
var hasMapping = false
buttons.forEach {
if (it != "[empty]" && it.isNotEmpty()) {
hasMapping = true
}
}
analogs.forEach {
if (it != "[empty]" && it.isNotEmpty()) {
hasMapping = true
}
}
motions.forEach {
if (it != "[empty]" && it.isNotEmpty()) {
hasMapping = true
}
}
return hasMapping
}
}

View File

@ -25,8 +25,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
HAPTIC_FEEDBACK("haptic_feedback"), HAPTIC_FEEDBACK("haptic_feedback"),
SHOW_PERFORMANCE_OVERLAY("show_performance_overlay"), SHOW_PERFORMANCE_OVERLAY("show_performance_overlay"),
SHOW_INPUT_OVERLAY("show_input_overlay"), SHOW_INPUT_OVERLAY("show_input_overlay"),
TOUCHSCREEN("touchscreen"), TOUCHSCREEN("touchscreen");
SHOW_THERMAL_OVERLAY("show_thermal_overlay");
override fun getBoolean(needsGlobal: Boolean): Boolean = override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeConfig.getBoolean(key, needsGlobal) NativeConfig.getBoolean(key, needsGlobal)

View File

@ -24,9 +24,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
THEME_MODE("theme_mode"), THEME_MODE("theme_mode"),
OVERLAY_SCALE("control_scale"), OVERLAY_SCALE("control_scale"),
OVERLAY_OPACITY("control_opacity"), OVERLAY_OPACITY("control_opacity"),
LOCK_DRAWER("lock_drawer"), LOCK_DRAWER("lock_drawer");
VERTICAL_ALIGNMENT("vertical_alignment"),
FSR_SHARPENING_SLIDER("fsr_sharpening_slider");
override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal) override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)

View File

@ -4,30 +4,17 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
object Settings { object Settings {
enum class MenuTag(val titleId: Int = 0) { enum class MenuTag(val titleId: Int) {
SECTION_ROOT(R.string.advanced_settings), SECTION_ROOT(R.string.advanced_settings),
SECTION_SYSTEM(R.string.preferences_system), SECTION_SYSTEM(R.string.preferences_system),
SECTION_RENDERER(R.string.preferences_graphics), SECTION_RENDERER(R.string.preferences_graphics),
SECTION_AUDIO(R.string.preferences_audio), SECTION_AUDIO(R.string.preferences_audio),
SECTION_INPUT(R.string.preferences_controls),
SECTION_INPUT_PLAYER_ONE,
SECTION_INPUT_PLAYER_TWO,
SECTION_INPUT_PLAYER_THREE,
SECTION_INPUT_PLAYER_FOUR,
SECTION_INPUT_PLAYER_FIVE,
SECTION_INPUT_PLAYER_SIX,
SECTION_INPUT_PLAYER_SEVEN,
SECTION_INPUT_PLAYER_EIGHT,
SECTION_THEME(R.string.preferences_theme), SECTION_THEME(R.string.preferences_theme),
SECTION_DEBUG(R.string.preferences_debug); SECTION_DEBUG(R.string.preferences_debug);
} }
fun getPlayerString(player: Int): String =
YuzuApplication.appContext.getString(R.string.preferences_player, player)
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown" const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown"
@ -106,15 +93,4 @@ object Settings {
entries.firstOrNull { it.int == int } ?: Unspecified entries.firstOrNull { it.int == int } ?: Unspecified
} }
} }
enum class EmulationVerticalAlignment(val int: Int) {
Top(1),
Center(0),
Bottom(2);
companion object {
fun from(int: Int): EmulationVerticalAlignment =
entries.firstOrNull { it.int == int } ?: Center
}
}
} }

View File

@ -6,8 +6,7 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
enum class StringSetting(override val key: String) : AbstractStringSetting { enum class StringSetting(override val key: String) : AbstractStringSetting {
DRIVER_PATH("driver_path"), DRIVER_PATH("driver_path");
DEVICE_NAME("device_name");
override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal) override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal)

View File

@ -1,31 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
import org.yuzu.yuzu_emu.features.input.model.InputType
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
import org.yuzu.yuzu_emu.utils.ParamPackage
class AnalogInputSetting(
override val playerIndex: Int,
val nativeAnalog: NativeAnalog,
val analogDirection: AnalogDirection,
@StringRes titleId: Int = 0,
titleString: String = ""
) : InputSetting(titleId, titleString) {
override val type = TYPE_INPUT
override val inputType = InputType.Stick
override fun getSelectedValue(): String {
val params = NativeInput.getStickParam(playerIndex, nativeAnalog)
val analog = analogToText(params, analogDirection.param)
return getDisplayString(params, analog)
}
override fun setSelectedValue(param: ParamPackage) =
NativeInput.setStickParam(playerIndex, nativeAnalog, param)
}

View File

@ -1,29 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.utils.ParamPackage
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.InputType
import org.yuzu.yuzu_emu.features.input.model.NativeButton
class ButtonInputSetting(
override val playerIndex: Int,
val nativeButton: NativeButton,
@StringRes titleId: Int = 0,
titleString: String = ""
) : InputSetting(titleId, titleString) {
override val type = TYPE_INPUT
override val inputType = InputType.Button
override fun getSelectedValue(): String {
val params = NativeInput.getButtonParam(playerIndex, nativeButton)
val button = buttonToText(params)
return getDisplayString(params, button)
}
override fun setSelectedValue(param: ParamPackage) =
NativeInput.setButtonParam(playerIndex, nativeButton, param)
}

View File

@ -3,16 +3,13 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting
class DateTimeSetting( class DateTimeSetting(
private val longSetting: AbstractLongSetting, private val longSetting: AbstractLongSetting,
@StringRes titleId: Int = 0, titleId: Int,
titleString: String = "", descriptionId: Int
@StringRes descriptionId: Int = 0, ) : SettingsItem(longSetting, titleId, descriptionId) {
descriptionString: String = ""
) : SettingsItem(longSetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_DATETIME_SETTING override val type = TYPE_DATETIME_SETTING
fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal) fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal)

View File

@ -3,11 +3,8 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
class HeaderSetting( class HeaderSetting(
@StringRes titleId: Int = 0, titleId: Int
titleString: String = "" ) : SettingsItem(emptySetting, titleId, 0) {
) : SettingsItem(emptySetting, titleId, titleString, 0, "") {
override val type = TYPE_HEADER override val type = TYPE_HEADER
} }

View File

@ -1,32 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.utils.NativeConfig
class InputProfileSetting(private val playerIndex: Int) :
SettingsItem(emptySetting, R.string.profile, "", 0, "") {
override val type = TYPE_INPUT_PROFILE
fun getCurrentProfile(): String =
NativeConfig.getInputSettings(true)[playerIndex].profileName
fun getProfileNames(): Array<String> = NativeInput.getInputProfileNames()
fun isProfileNameValid(name: String): Boolean = NativeInput.isProfileNameValid(name)
fun createProfile(name: String): Boolean = NativeInput.createProfile(name, playerIndex)
fun deleteProfile(name: String): Boolean = NativeInput.deleteProfile(name, playerIndex)
fun loadProfile(name: String): Boolean {
val result = NativeInput.loadProfile(name, playerIndex)
NativeInput.reloadInputDevices()
return result
}
fun saveProfile(name: String): Boolean = NativeInput.saveProfile(name, playerIndex)
}

View File

@ -1,134 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.ButtonName
import org.yuzu.yuzu_emu.features.input.model.InputType
import org.yuzu.yuzu_emu.utils.ParamPackage
sealed class InputSetting(
@StringRes titleId: Int,
titleString: String
) : SettingsItem(emptySetting, titleId, titleString, 0, "") {
override val type = TYPE_INPUT
abstract val inputType: InputType
abstract val playerIndex: Int
protected val context get() = YuzuApplication.appContext
abstract fun getSelectedValue(): String
abstract fun setSelectedValue(param: ParamPackage)
protected fun getDisplayString(params: ParamPackage, control: String): String {
val deviceName = params.get("display", "")
deviceName.ifEmpty {
return context.getString(R.string.not_set)
}
return "$deviceName: $control"
}
private fun getDirectionName(direction: String): String =
when (direction) {
"up" -> context.getString(R.string.up)
"down" -> context.getString(R.string.down)
"left" -> context.getString(R.string.left)
"right" -> context.getString(R.string.right)
else -> direction
}
protected fun buttonToText(param: ParamPackage): String {
if (!param.has("engine")) {
return context.getString(R.string.not_set)
}
val toggle = if (param.get("toggle", false)) "~" else ""
val inverted = if (param.get("inverted", false)) "!" else ""
val invert = if (param.get("invert", "+") == "-") "-" else ""
val turbo = if (param.get("turbo", false)) "$" else ""
val commonButtonName = NativeInput.getButtonName(param)
if (commonButtonName == ButtonName.Invalid) {
return context.getString(R.string.invalid)
}
if (commonButtonName == ButtonName.Engine) {
return param.get("engine", "")
}
if (commonButtonName == ButtonName.Value) {
if (param.has("hat")) {
val hat = getDirectionName(param.get("direction", ""))
return context.getString(R.string.qualified_hat, turbo, toggle, inverted, hat)
}
if (param.has("axis")) {
val axis = param.get("axis", "")
return context.getString(
R.string.qualified_button_stick_axis,
toggle,
inverted,
invert,
axis
)
}
if (param.has("button")) {
val button = param.get("button", "")
return context.getString(R.string.qualified_button, turbo, toggle, inverted, button)
}
}
return context.getString(R.string.unknown)
}
protected fun analogToText(param: ParamPackage, direction: String): String {
if (!param.has("engine")) {
return context.getString(R.string.not_set)
}
if (param.get("engine", "") == "analog_from_button") {
return buttonToText(ParamPackage(param.get(direction, "")))
}
if (!param.has("axis_x") || !param.has("axis_y")) {
return context.getString(R.string.unknown)
}
val xAxis = param.get("axis_x", "")
val yAxis = param.get("axis_y", "")
val xInvert = param.get("invert_x", "+") == "-"
val yInvert = param.get("invert_y", "+") == "-"
if (direction == "modifier") {
return context.getString(R.string.unused)
}
when (direction) {
"up" -> {
val yInvertString = if (yInvert) "+" else "-"
return context.getString(R.string.qualified_axis, yAxis, yInvertString)
}
"down" -> {
val yInvertString = if (yInvert) "-" else "+"
return context.getString(R.string.qualified_axis, yAxis, yInvertString)
}
"left" -> {
val xInvertString = if (xInvert) "+" else "-"
return context.getString(R.string.qualified_axis, xAxis, xInvertString)
}
"right" -> {
val xInvertString = if (xInvert) "-" else "+"
return context.getString(R.string.qualified_axis, xAxis, xInvertString)
}
}
return context.getString(R.string.unknown)
}
}

View File

@ -1,38 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
class IntSingleChoiceSetting(
private val intSetting: AbstractIntSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
val choices: Array<String>,
val values: Array<Int>
) : SettingsItem(intSetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_INT_SINGLE_CHOICE
fun getValueAt(index: Int): Int =
if (values.indices.contains(index)) values[index] else -1
fun getChoiceAt(index: Int): String =
if (choices.indices.contains(index)) choices[index] else ""
fun getSelectedValue(needsGlobal: Boolean = false) = intSetting.getInt(needsGlobal)
fun setSelectedValue(value: Int) = intSetting.setInt(value)
val selectedValueIndex: Int
get() {
for (i in values.indices) {
if (values[i] == getSelectedValue()) {
return i
}
}
return -1
}
}

View File

@ -1,31 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.InputType
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
import org.yuzu.yuzu_emu.utils.ParamPackage
class ModifierInputSetting(
override val playerIndex: Int,
val nativeAnalog: NativeAnalog,
@StringRes titleId: Int = 0,
titleString: String = ""
) : InputSetting(titleId, titleString) {
override val inputType = InputType.Button
override fun getSelectedValue(): String {
val analogParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
val modifierParam = ParamPackage(analogParam.get("modifier", ""))
return buttonToText(modifierParam)
}
override fun setSelectedValue(param: ParamPackage) {
val newParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
newParam.set("modifier", param.serialize())
NativeInput.setStickParam(playerIndex, nativeAnalog, newParam)
}
}

View File

@ -4,16 +4,13 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
class RunnableSetting( class RunnableSetting(
@StringRes titleId: Int = 0, titleId: Int,
titleString: String = "", descriptionId: Int,
@StringRes descriptionId: Int = 0, val isRuntimeRunnable: Boolean,
descriptionString: String = "",
val isRunnable: Boolean,
@DrawableRes val iconId: Int = 0, @DrawableRes val iconId: Int = 0,
val runnable: () -> Unit val runnable: () -> Unit
) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) { ) : SettingsItem(emptySetting, titleId, descriptionId) {
override val type = TYPE_RUNNABLE override val type = TYPE_RUNNABLE
} }

View File

@ -3,12 +3,8 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
@ -16,7 +12,6 @@ import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.LongSetting import org.yuzu.yuzu_emu.features.settings.model.LongSetting
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
/** /**
@ -28,34 +23,13 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
*/ */
abstract class SettingsItem( abstract class SettingsItem(
val setting: AbstractSetting, val setting: AbstractSetting,
@StringRes val titleId: Int, val nameId: Int,
val titleString: String, val descriptionId: Int
@StringRes val descriptionId: Int,
val descriptionString: String
) { ) {
abstract val type: Int abstract val type: Int
val title: String by lazy {
if (titleId != 0) {
return@lazy YuzuApplication.appContext.getString(titleId)
}
return@lazy titleString
}
val description: String by lazy {
if (descriptionId != 0) {
return@lazy YuzuApplication.appContext.getString(descriptionId)
}
return@lazy descriptionString
}
val isEditable: Boolean val isEditable: Boolean
get() { get() {
// Can't change docked mode toggle when using handheld mode
if (setting.key == BooleanSetting.USE_DOCKED_MODE.key) {
return NativeInput.getStyleIndex(0) != NpadStyleIndex.Handheld
}
// Can't edit settings that aren't saveable in per-game config even if they are switchable // Can't edit settings that aren't saveable in per-game config even if they are switchable
if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) { if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
return false return false
@ -76,9 +50,6 @@ abstract class SettingsItem(
get() = NativeLibrary.isRunning() && !setting.global && get() = NativeLibrary.isRunning() && !setting.global &&
!NativeConfig.isPerGameConfigLoaded() !NativeConfig.isPerGameConfigLoaded()
val clearable: Boolean
get() = !setting.global && NativeConfig.isPerGameConfigLoaded()
companion object { companion object {
const val TYPE_HEADER = 0 const val TYPE_HEADER = 0
const val TYPE_SWITCH = 1 const val TYPE_SWITCH = 1
@ -88,10 +59,6 @@ abstract class SettingsItem(
const val TYPE_STRING_SINGLE_CHOICE = 5 const val TYPE_STRING_SINGLE_CHOICE = 5
const val TYPE_DATETIME_SETTING = 6 const val TYPE_DATETIME_SETTING = 6
const val TYPE_RUNNABLE = 7 const val TYPE_RUNNABLE = 7
const val TYPE_INPUT = 8
const val TYPE_INT_SINGLE_CHOICE = 9
const val TYPE_INPUT_PROFILE = 10
const val TYPE_STRING_INPUT = 11
const val FASTMEM_COMBINED = "fastmem_combined" const val FASTMEM_COMBINED = "fastmem_combined"
@ -110,246 +77,221 @@ abstract class SettingsItem(
// List of all general // List of all general
val settingsItems = HashMap<String, SettingsItem>().apply { val settingsItems = HashMap<String, SettingsItem>().apply {
put(StringInputSetting(StringSetting.DEVICE_NAME, titleId = R.string.device_name))
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.RENDERER_USE_SPEED_LIMIT, BooleanSetting.RENDERER_USE_SPEED_LIMIT,
titleId = R.string.frame_limit_enable, R.string.frame_limit_enable,
descriptionId = R.string.frame_limit_enable_description R.string.frame_limit_enable_description
) )
) )
put( put(
SliderSetting( SliderSetting(
ShortSetting.RENDERER_SPEED_LIMIT, ShortSetting.RENDERER_SPEED_LIMIT,
titleId = R.string.frame_limit_slider, R.string.frame_limit_slider,
descriptionId = R.string.frame_limit_slider_description, R.string.frame_limit_slider_description,
min = 1, 1,
max = 400, 400,
units = "%" "%"
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.CPU_BACKEND, IntSetting.CPU_BACKEND,
titleId = R.string.cpu_backend, R.string.cpu_backend,
choicesId = R.array.cpuBackendArm64Names, 0,
valuesId = R.array.cpuBackendArm64Values R.array.cpuBackendArm64Names,
R.array.cpuBackendArm64Values
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.CPU_ACCURACY, IntSetting.CPU_ACCURACY,
titleId = R.string.cpu_accuracy, R.string.cpu_accuracy,
choicesId = R.array.cpuAccuracyNames, 0,
valuesId = R.array.cpuAccuracyValues R.array.cpuAccuracyNames,
R.array.cpuAccuracyValues
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.PICTURE_IN_PICTURE, BooleanSetting.PICTURE_IN_PICTURE,
titleId = R.string.picture_in_picture, R.string.picture_in_picture,
descriptionId = R.string.picture_in_picture_description R.string.picture_in_picture_description
) )
) )
val dockedModeSetting = object : AbstractBooleanSetting {
override val key = BooleanSetting.USE_DOCKED_MODE.key
override fun getBoolean(needsGlobal: Boolean): Boolean {
if (NativeInput.getStyleIndex(0) == NpadStyleIndex.Handheld) {
return false
}
return BooleanSetting.USE_DOCKED_MODE.getBoolean(needsGlobal)
}
override fun setBoolean(value: Boolean) =
BooleanSetting.USE_DOCKED_MODE.setBoolean(value)
override val defaultValue = BooleanSetting.USE_DOCKED_MODE.defaultValue
override fun getValueAsString(needsGlobal: Boolean): String =
BooleanSetting.USE_DOCKED_MODE.getValueAsString(needsGlobal)
override fun reset() = BooleanSetting.USE_DOCKED_MODE.reset()
}
put( put(
SwitchSetting( SwitchSetting(
dockedModeSetting, BooleanSetting.USE_DOCKED_MODE,
titleId = R.string.use_docked_mode, R.string.use_docked_mode,
descriptionId = R.string.use_docked_mode_description R.string.use_docked_mode_description
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.REGION_INDEX, IntSetting.REGION_INDEX,
titleId = R.string.emulated_region, R.string.emulated_region,
choicesId = R.array.regionNames, 0,
valuesId = R.array.regionValues R.array.regionNames,
R.array.regionValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.LANGUAGE_INDEX, IntSetting.LANGUAGE_INDEX,
titleId = R.string.emulated_language, R.string.emulated_language,
choicesId = R.array.languageNames, 0,
valuesId = R.array.languageValues R.array.languageNames,
R.array.languageValues
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.USE_CUSTOM_RTC, BooleanSetting.USE_CUSTOM_RTC,
titleId = R.string.use_custom_rtc, R.string.use_custom_rtc,
descriptionId = R.string.use_custom_rtc_description R.string.use_custom_rtc_description
) )
) )
put(DateTimeSetting(LongSetting.CUSTOM_RTC, titleId = R.string.set_custom_rtc)) put(DateTimeSetting(LongSetting.CUSTOM_RTC, R.string.set_custom_rtc, 0))
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_ACCURACY, IntSetting.RENDERER_ACCURACY,
titleId = R.string.renderer_accuracy, R.string.renderer_accuracy,
choicesId = R.array.rendererAccuracyNames, 0,
valuesId = R.array.rendererAccuracyValues R.array.rendererAccuracyNames,
R.array.rendererAccuracyValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_RESOLUTION, IntSetting.RENDERER_RESOLUTION,
titleId = R.string.renderer_resolution, R.string.renderer_resolution,
choicesId = R.array.rendererResolutionNames, 0,
valuesId = R.array.rendererResolutionValues R.array.rendererResolutionNames,
R.array.rendererResolutionValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_VSYNC, IntSetting.RENDERER_VSYNC,
titleId = R.string.renderer_vsync, R.string.renderer_vsync,
choicesId = R.array.rendererVSyncNames, 0,
valuesId = R.array.rendererVSyncValues R.array.rendererVSyncNames,
R.array.rendererVSyncValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_SCALING_FILTER, IntSetting.RENDERER_SCALING_FILTER,
titleId = R.string.renderer_scaling_filter, R.string.renderer_scaling_filter,
choicesId = R.array.rendererScalingFilterNames, 0,
valuesId = R.array.rendererScalingFilterValues R.array.rendererScalingFilterNames,
) R.array.rendererScalingFilterValues
)
put(
SliderSetting(
IntSetting.FSR_SHARPENING_SLIDER,
titleId = R.string.fsr_sharpness,
descriptionId = R.string.fsr_sharpness_description,
units = "%"
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_ANTI_ALIASING, IntSetting.RENDERER_ANTI_ALIASING,
titleId = R.string.renderer_anti_aliasing, R.string.renderer_anti_aliasing,
choicesId = R.array.rendererAntiAliasingNames, 0,
valuesId = R.array.rendererAntiAliasingValues R.array.rendererAntiAliasingNames,
R.array.rendererAntiAliasingValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_SCREEN_LAYOUT, IntSetting.RENDERER_SCREEN_LAYOUT,
titleId = R.string.renderer_screen_layout, R.string.renderer_screen_layout,
choicesId = R.array.rendererScreenLayoutNames, 0,
valuesId = R.array.rendererScreenLayoutValues R.array.rendererScreenLayoutNames,
R.array.rendererScreenLayoutValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_ASPECT_RATIO, IntSetting.RENDERER_ASPECT_RATIO,
titleId = R.string.renderer_aspect_ratio, R.string.renderer_aspect_ratio,
choicesId = R.array.rendererAspectRatioNames, 0,
valuesId = R.array.rendererAspectRatioValues R.array.rendererAspectRatioNames,
) R.array.rendererAspectRatioValues
)
put(
SingleChoiceSetting(
IntSetting.VERTICAL_ALIGNMENT,
titleId = R.string.vertical_alignment,
descriptionId = 0,
choicesId = R.array.verticalAlignmentEntries,
valuesId = R.array.verticalAlignmentValues
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE, BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE,
titleId = R.string.use_disk_shader_cache, R.string.use_disk_shader_cache,
descriptionId = R.string.use_disk_shader_cache_description R.string.use_disk_shader_cache_description
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.RENDERER_FORCE_MAX_CLOCK, BooleanSetting.RENDERER_FORCE_MAX_CLOCK,
titleId = R.string.renderer_force_max_clock, R.string.renderer_force_max_clock,
descriptionId = R.string.renderer_force_max_clock_description R.string.renderer_force_max_clock_description
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS, BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS,
titleId = R.string.renderer_asynchronous_shaders, R.string.renderer_asynchronous_shaders,
descriptionId = R.string.renderer_asynchronous_shaders_description R.string.renderer_asynchronous_shaders_description
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.RENDERER_REACTIVE_FLUSHING, BooleanSetting.RENDERER_REACTIVE_FLUSHING,
titleId = R.string.renderer_reactive_flushing, R.string.renderer_reactive_flushing,
descriptionId = R.string.renderer_reactive_flushing_description R.string.renderer_reactive_flushing_description
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.MAX_ANISOTROPY, IntSetting.MAX_ANISOTROPY,
titleId = R.string.anisotropic_filtering, R.string.anisotropic_filtering,
descriptionId = R.string.anisotropic_filtering_description, R.string.anisotropic_filtering_description,
choicesId = R.array.anisoEntries, R.array.anisoEntries,
valuesId = R.array.anisoValues R.array.anisoValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.AUDIO_OUTPUT_ENGINE, IntSetting.AUDIO_OUTPUT_ENGINE,
titleId = R.string.audio_output_engine, R.string.audio_output_engine,
choicesId = R.array.outputEngineEntries, 0,
valuesId = R.array.outputEngineValues R.array.outputEngineEntries,
R.array.outputEngineValues
) )
) )
put( put(
SliderSetting( SliderSetting(
ByteSetting.AUDIO_VOLUME, ByteSetting.AUDIO_VOLUME,
titleId = R.string.audio_volume, R.string.audio_volume,
descriptionId = R.string.audio_volume_description, R.string.audio_volume_description,
units = "%" 0,
100,
"%"
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_BACKEND, IntSetting.RENDERER_BACKEND,
titleId = R.string.renderer_api, R.string.renderer_api,
choicesId = R.array.rendererApiNames, 0,
valuesId = R.array.rendererApiValues R.array.rendererApiNames,
R.array.rendererApiValues
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.RENDERER_DEBUG, BooleanSetting.RENDERER_DEBUG,
titleId = R.string.renderer_debug, R.string.renderer_debug,
descriptionId = R.string.renderer_debug_description R.string.renderer_debug_description
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.CPU_DEBUG_MODE, BooleanSetting.CPU_DEBUG_MODE,
titleId = R.string.cpu_debug_mode, R.string.cpu_debug_mode,
descriptionId = R.string.cpu_debug_mode_description R.string.cpu_debug_mode_description
) )
) )
@ -385,7 +327,7 @@ abstract class SettingsItem(
override fun reset() = setBoolean(defaultValue) override fun reset() = setBoolean(defaultValue)
} }
put(SwitchSetting(fastmem, R.string.fastmem)) put(SwitchSetting(fastmem, R.string.fastmem, 0))
} }
} }
} }

View File

@ -3,20 +3,16 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.ArrayRes
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
class SingleChoiceSetting( class SingleChoiceSetting(
setting: AbstractSetting, setting: AbstractSetting,
@StringRes titleId: Int = 0, titleId: Int,
titleString: String = "", descriptionId: Int,
@StringRes descriptionId: Int = 0, val choicesId: Int,
descriptionString: String = "", val valuesId: Int
@ArrayRes val choicesId: Int, ) : SettingsItem(setting, titleId, descriptionId) {
@ArrayRes val valuesId: Int
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_SINGLE_CHOICE override val type = TYPE_SINGLE_CHOICE
fun getSelectedValue(needsGlobal: Boolean = false) = fun getSelectedValue(needsGlobal: Boolean = false) =

View File

@ -3,7 +3,6 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
@ -13,14 +12,12 @@ import kotlin.math.roundToInt
class SliderSetting( class SliderSetting(
setting: AbstractSetting, setting: AbstractSetting,
@StringRes titleId: Int = 0, titleId: Int,
titleString: String = "", descriptionId: Int,
@StringRes descriptionId: Int = 0, val min: Int,
descriptionString: String = "", val max: Int,
val min: Int = 0, val units: String
val max: Int = 100, ) : SettingsItem(setting, titleId, descriptionId) {
val units: String = ""
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_SLIDER override val type = TYPE_SLIDER
fun getSelectedValue(needsGlobal: Boolean = false) = fun getSelectedValue(needsGlobal: Boolean = false) =

View File

@ -1,22 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
class StringInputSetting(
setting: AbstractStringSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = ""
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_STRING_INPUT
fun getSelectedValue(needsGlobal: Boolean = false) = setting.getValueAsString(needsGlobal)
fun setSelectedValue(selection: String) =
(setting as AbstractStringSetting).setString(selection)
}

View File

@ -3,18 +3,15 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
class StringSingleChoiceSetting( class StringSingleChoiceSetting(
private val stringSetting: AbstractStringSetting, private val stringSetting: AbstractStringSetting,
@StringRes titleId: Int = 0, titleId: Int,
titleString: String = "", descriptionId: Int,
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
val choices: Array<String>, val choices: Array<String>,
val values: Array<String> val values: Array<String>
) : SettingsItem(stringSetting, titleId, titleString, descriptionId, descriptionString) { ) : SettingsItem(stringSetting, titleId, descriptionId) {
override val type = TYPE_STRING_SINGLE_CHOICE override val type = TYPE_STRING_SINGLE_CHOICE
fun getValueAt(index: Int): String = fun getValueAt(index: Int): String =
@ -23,7 +20,7 @@ class StringSingleChoiceSetting(
fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal) fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)
fun setSelectedValue(value: String) = stringSetting.setString(value) fun setSelectedValue(value: String) = stringSetting.setString(value)
val selectedValueIndex: Int val selectValueIndex: Int
get() { get() {
for (i in values.indices) { for (i in values.indices) {
if (values[i] == getSelectedValue()) { if (values[i] == getSelectedValue()) {

View File

@ -8,12 +8,10 @@ import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
class SubmenuSetting( class SubmenuSetting(
@StringRes titleId: Int = 0, @StringRes titleId: Int,
titleString: String = "", @StringRes descriptionId: Int,
@StringRes descriptionId: Int = 0, @DrawableRes val iconId: Int,
descriptionString: String = "",
@DrawableRes val iconId: Int = 0,
val menuKey: Settings.MenuTag val menuKey: Settings.MenuTag
) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) { ) : SettingsItem(emptySetting, titleId, descriptionId) {
override val type = TYPE_SUBMENU override val type = TYPE_SUBMENU
} }

View File

@ -3,18 +3,15 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
class SwitchSetting( class SwitchSetting(
setting: AbstractSetting, setting: AbstractSetting,
@StringRes titleId: Int = 0, titleId: Int,
titleString: String = "", descriptionId: Int
@StringRes descriptionId: Int = 0, ) : SettingsItem(setting, titleId, descriptionId) {
descriptionString: String = ""
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_SWITCH override val type = TYPE_SWITCH
fun getIsChecked(needsGlobal: Boolean = false): Boolean { fun getIsChecked(needsGlobal: Boolean = false): Boolean {

View File

@ -1,300 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.app.Dialog
import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogMappingBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
import org.yuzu.yuzu_emu.features.input.model.NativeButton
import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting
import org.yuzu.yuzu_emu.utils.InputHandler
import org.yuzu.yuzu_emu.utils.ParamPackage
class InputDialogFragment : DialogFragment() {
private var inputAccepted = false
private var position: Int = 0
private lateinit var inputSetting: InputSetting
private lateinit var binding: DialogMappingBinding
private val settingsViewModel: SettingsViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (settingsViewModel.clickedItem == null) dismiss()
position = requireArguments().getInt(POSITION)
InputHandler.updateControllerData()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
inputSetting = settingsViewModel.clickedItem as InputSetting
binding = DialogMappingBinding.inflate(layoutInflater)
val builder = MaterialAlertDialogBuilder(requireContext())
.setPositiveButton(android.R.string.cancel) { _, _ ->
NativeInput.stopMapping()
dismiss()
}
.setView(binding.root)
val playButtonMapAnimation = { twoDirections: Boolean ->
val stickAnimation: AnimatedVectorDrawable
val buttonAnimation: AnimatedVectorDrawable
binding.imageStickAnimation.apply {
val anim = if (twoDirections) {
R.drawable.stick_two_direction_anim
} else {
R.drawable.stick_one_direction_anim
}
setBackgroundResource(anim)
stickAnimation = background as AnimatedVectorDrawable
}
binding.imageButtonAnimation.apply {
setBackgroundResource(R.drawable.button_anim)
buttonAnimation = background as AnimatedVectorDrawable
}
stickAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
buttonAnimation.start()
}
})
buttonAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
stickAnimation.start()
}
})
stickAnimation.start()
}
when (val setting = inputSetting) {
is AnalogInputSetting -> {
when (setting.nativeAnalog) {
NativeAnalog.LStick -> builder.setTitle(
getString(R.string.map_control, getString(R.string.left_stick))
)
NativeAnalog.RStick -> builder.setTitle(
getString(R.string.map_control, getString(R.string.right_stick))
)
}
builder.setMessage(R.string.stick_map_description)
playButtonMapAnimation.invoke(true)
}
is ModifierInputSetting -> {
builder.setTitle(getString(R.string.map_control, setting.title))
.setMessage(R.string.button_map_description)
playButtonMapAnimation.invoke(false)
}
is ButtonInputSetting -> {
if (setting.nativeButton == NativeButton.DUp ||
setting.nativeButton == NativeButton.DDown ||
setting.nativeButton == NativeButton.DLeft ||
setting.nativeButton == NativeButton.DRight
) {
builder.setTitle(getString(R.string.map_dpad_direction, setting.title))
} else {
builder.setTitle(getString(R.string.map_control, setting.title))
}
builder.setMessage(R.string.button_map_description)
playButtonMapAnimation.invoke(false)
}
}
return builder.create()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.requestFocus()
view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
dialog?.setOnKeyListener { _, _, keyEvent -> onKeyEvent(keyEvent) }
binding.root.setOnGenericMotionListener { _, motionEvent -> onMotionEvent(motionEvent) }
NativeInput.beginMapping(inputSetting.inputType.int)
}
private fun onKeyEvent(event: KeyEvent): Boolean {
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
) {
return false
}
val action = when (event.action) {
KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED
KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED
else -> return false
}
val controllerData =
InputHandler.androidControllers[event.device.controllerNumber] ?: return false
NativeInput.onGamePadButtonEvent(
controllerData.getGUID(),
controllerData.getPort(),
event.keyCode,
action
)
onInputReceived(event.device)
return true
}
private fun onMotionEvent(event: MotionEvent): Boolean {
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
) {
return false
}
// Temp workaround for DPads that give both axis and button input. The input system can't
// take in a specific axis direction for a binding so you lose half of the directions for a DPad.
val controllerData =
InputHandler.androidControllers[event.device.controllerNumber] ?: return false
event.device.motionRanges.forEach {
NativeInput.onGamePadAxisEvent(
controllerData.getGUID(),
controllerData.getPort(),
it.axis,
event.getAxisValue(it.axis)
)
onInputReceived(event.device)
}
return true
}
private fun onInputReceived(device: InputDevice) {
val params = ParamPackage(NativeInput.getNextInput())
if (params.has("engine") && isInputAcceptable(params) && !inputAccepted) {
inputAccepted = true
setResult(params, device)
}
}
private fun setResult(params: ParamPackage, device: InputDevice) {
NativeInput.stopMapping()
params.set("display", "${device.name} ${params.get("port", 0)}")
when (val item = settingsViewModel.clickedItem as InputSetting) {
is ModifierInputSetting,
is ButtonInputSetting -> {
// Invert DPad up and left bindings by default
val tempSetting = inputSetting as? ButtonInputSetting
if (tempSetting != null) {
if (tempSetting.nativeButton == NativeButton.DUp ||
tempSetting.nativeButton == NativeButton.DLeft &&
params.has("axis")
) {
params.set("invert", "-")
}
}
item.setSelectedValue(params)
settingsViewModel.setAdapterItemChanged(position)
}
is AnalogInputSetting -> {
var analogParam = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
analogParam = adjustAnalogParam(params, analogParam, item.analogDirection.param)
// Invert Y-Axis by default
analogParam.set("invert_y", "-")
item.setSelectedValue(analogParam)
settingsViewModel.setReloadListAndNotifyDataset(true)
}
}
dismiss()
}
private fun adjustAnalogParam(
inputParam: ParamPackage,
analogParam: ParamPackage,
buttonName: String
): ParamPackage {
// The poller returned a complete axis, so set all the buttons
if (inputParam.has("axis_x") && inputParam.has("axis_y")) {
return inputParam
}
// Check if the current configuration has either no engine or an axis binding.
// Clears out the old binding and adds one with analog_from_button.
if (!analogParam.has("engine") || analogParam.has("axis_x") || analogParam.has("axis_y")) {
analogParam.clear()
analogParam.set("engine", "analog_from_button")
}
analogParam.set(buttonName, inputParam.serialize())
return analogParam
}
private fun isInputAcceptable(params: ParamPackage): Boolean {
if (InputHandler.registeredControllers.size == 1) {
return true
}
if (params.has("motion")) {
return true
}
val currentDevice = settingsViewModel.getCurrentDeviceParams(params)
if (currentDevice.get("engine", "any") == "any") {
return true
}
val guidMatch = params.get("guid", "") == currentDevice.get("guid", "") ||
params.get("guid", "") == currentDevice.get("guid2", "")
return params.get("engine", "") == currentDevice.get("engine", "") &&
guidMatch &&
params.get("port", 0) == currentDevice.get("port", 0)
}
companion object {
const val TAG = "InputDialogFragment"
const val POSITION = "Position"
fun newInstance(
inputMappingViewModel: SettingsViewModel,
setting: InputSetting,
position: Int
): InputDialogFragment {
inputMappingViewModel.clickedItem = setting
val args = Bundle()
args.putInt(POSITION, position)
val fragment = InputDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -1,68 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.AbstractListAdapter
import org.yuzu.yuzu_emu.databinding.ListItemInputProfileBinding
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
import org.yuzu.yuzu_emu.R
class InputProfileAdapter(options: List<ProfileItem>) :
AbstractListAdapter<ProfileItem, AbstractViewHolder<ProfileItem>>(options) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): AbstractViewHolder<ProfileItem> {
ListItemInputProfileBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return InputProfileViewHolder(it) }
}
inner class InputProfileViewHolder(val binding: ListItemInputProfileBinding) :
AbstractViewHolder<ProfileItem>(binding) {
override fun bind(model: ProfileItem) {
when (model) {
is ExistingProfileItem -> {
binding.title.text = model.name
binding.buttonNew.visibility = View.GONE
binding.buttonDelete.visibility = View.VISIBLE
binding.buttonDelete.setOnClickListener { model.deleteProfile.invoke() }
binding.buttonSave.visibility = View.VISIBLE
binding.buttonSave.setOnClickListener { model.saveProfile.invoke() }
binding.buttonLoad.visibility = View.VISIBLE
binding.buttonLoad.setOnClickListener { model.loadProfile.invoke() }
}
is NewProfileItem -> {
binding.title.text = model.name
binding.buttonNew.visibility = View.VISIBLE
binding.buttonNew.setOnClickListener { model.createNewProfile.invoke() }
binding.buttonSave.visibility = View.GONE
binding.buttonDelete.visibility = View.GONE
binding.buttonLoad.visibility = View.GONE
}
}
}
}
}
sealed interface ProfileItem {
val name: String
}
data class NewProfileItem(
val createNewProfile: () -> Unit
) : ProfileItem {
override val name: String = YuzuApplication.appContext.getString(R.string.create_new_profile)
}
data class ExistingProfileItem(
override val name: String,
val deleteProfile: () -> Unit,
val saveProfile: () -> Unit,
val loadProfile: () -> Unit
) : ProfileItem

View File

@ -1,148 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogInputProfilesBinding
import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.utils.collect
class InputProfileDialogFragment : DialogFragment() {
private var position = 0
private val settingsViewModel: SettingsViewModel by activityViewModels()
private lateinit var binding: DialogInputProfilesBinding
private lateinit var setting: InputProfileSetting
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
position = requireArguments().getInt(POSITION)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogInputProfilesBinding.inflate(layoutInflater)
setting = settingsViewModel.clickedItem as InputProfileSetting
val options = mutableListOf<ProfileItem>().apply {
add(
NewProfileItem(
createNewProfile = {
NewInputProfileDialogFragment.newInstance(
settingsViewModel,
setting,
position
).show(parentFragmentManager, NewInputProfileDialogFragment.TAG)
dismiss()
}
)
)
val onActionDismiss = {
settingsViewModel.setReloadListAndNotifyDataset(true)
dismiss()
}
setting.getProfileNames().forEach {
add(
ExistingProfileItem(
it,
deleteProfile = {
settingsViewModel.setShouldShowDeleteProfileDialog(it)
},
saveProfile = {
if (!setting.saveProfile(it)) {
Toast.makeText(
requireContext(),
R.string.failed_to_save_profile,
Toast.LENGTH_SHORT
).show()
}
onActionDismiss.invoke()
},
loadProfile = {
if (!setting.loadProfile(it)) {
Toast.makeText(
requireContext(),
R.string.failed_to_load_profile,
Toast.LENGTH_SHORT
).show()
}
onActionDismiss.invoke()
}
)
)
}
}
binding.listProfiles.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = InputProfileAdapter(options)
}
return MaterialAlertDialogBuilder(requireContext())
.setView(binding.root)
.create()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsViewModel.shouldShowDeleteProfileDialog.collect(viewLifecycleOwner) {
if (it.isNotEmpty()) {
MessageDialogFragment.newInstance(
activity = requireActivity(),
titleId = R.string.delete_input_profile,
descriptionId = R.string.delete_input_profile_description,
positiveAction = {
setting.deleteProfile(it)
settingsViewModel.setReloadListAndNotifyDataset(true)
},
negativeAction = {},
negativeButtonTitleId = android.R.string.cancel
).show(parentFragmentManager, MessageDialogFragment.TAG)
settingsViewModel.setShouldShowDeleteProfileDialog("")
dismiss()
}
}
}
companion object {
const val TAG = "InputProfileDialogFragment"
const val POSITION = "Position"
fun newInstance(
settingsViewModel: SettingsViewModel,
profileSetting: InputProfileSetting,
position: Int
): InputProfileDialogFragment {
settingsViewModel.clickedItem = profileSetting
val args = Bundle()
args.putInt(POSITION, position)
val fragment = InputProfileDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -1,79 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.app.Dialog
import android.os.Bundle
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding
import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
import org.yuzu.yuzu_emu.R
class NewInputProfileDialogFragment : DialogFragment() {
private var position = 0
private val settingsViewModel: SettingsViewModel by activityViewModels()
private lateinit var binding: DialogEditTextBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
position = requireArguments().getInt(POSITION)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogEditTextBinding.inflate(layoutInflater)
val setting = settingsViewModel.clickedItem as InputProfileSetting
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.enter_profile_name)
.setPositiveButton(android.R.string.ok) { _, _ ->
val profileName = binding.editText.text.toString()
if (!setting.isProfileNameValid(profileName)) {
Toast.makeText(
requireContext(),
R.string.invalid_profile_name,
Toast.LENGTH_SHORT
).show()
return@setPositiveButton
}
if (!setting.createProfile(profileName)) {
Toast.makeText(
requireContext(),
R.string.profile_name_already_exists,
Toast.LENGTH_SHORT
).show()
} else {
settingsViewModel.setAdapterItemChanged(position)
}
}
.setNegativeButton(android.R.string.cancel, null)
.setView(binding.root)
.show()
}
companion object {
const val TAG = "NewInputProfileDialogFragment"
const val POSITION = "Position"
fun newInstance(
settingsViewModel: SettingsViewModel,
profileSetting: InputProfileSetting,
position: Int
): NewInputProfileDialogFragment {
settingsViewModel.clickedItem = profileSetting
val args = Bundle()
args.putInt(POSITION, position)
val fragment = NewInputProfileDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -13,16 +13,21 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navArgs import androidx.navigation.navArgs
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import java.io.IOException import java.io.IOException
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
import org.yuzu.yuzu_emu.model.SettingsViewModel
import org.yuzu.yuzu_emu.utils.* import org.yuzu.yuzu_emu.utils.*
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
@ -65,23 +70,39 @@ class SettingsActivity : AppCompatActivity() {
) )
} }
settingsViewModel.shouldRecreate.collect( lifecycleScope.apply {
this, launch {
resetState = { settingsViewModel.setShouldRecreate(false) } repeatOnLifecycle(Lifecycle.State.CREATED) {
) { if (it) recreate() } settingsViewModel.shouldRecreate.collectLatest {
settingsViewModel.shouldNavigateBack.collect( if (it) {
this, settingsViewModel.setShouldRecreate(false)
resetState = { settingsViewModel.setShouldNavigateBack(false) } recreate()
) { if (it) navigateBack() } }
settingsViewModel.shouldShowResetSettingsDialog.collect( }
this, }
resetState = { settingsViewModel.setShouldShowResetSettingsDialog(false) } }
) { launch {
if (it) { repeatOnLifecycle(Lifecycle.State.CREATED) {
ResetSettingsDialogFragment().show( settingsViewModel.shouldNavigateBack.collectLatest {
supportFragmentManager, if (it) {
ResetSettingsDialogFragment.TAG settingsViewModel.setShouldNavigateBack(false)
) navigateBack()
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.shouldShowResetSettingsDialog.collectLatest {
if (it) {
settingsViewModel.setShouldShowResetSettingsDialog(false)
ResetSettingsDialogFragment().show(
supportFragmentManager,
ResetSettingsDialogFragment.TAG
)
}
}
}
} }
} }
@ -116,7 +137,6 @@ class SettingsActivity : AppCompatActivity() {
super.onStop() super.onStop()
Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...") Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
if (isFinishing) { if (isFinishing) {
NativeInput.reloadInputDevices()
NativeLibrary.applySettings() NativeLibrary.applySettings()
if (args.game == null) { if (args.game == null) {
NativeConfig.saveGlobalConfig() NativeConfig.saveGlobalConfig()

View File

@ -8,11 +8,12 @@ import android.icu.util.Calendar
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.text.format.DateFormat import android.text.format.DateFormat
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@ -20,18 +21,16 @@ import androidx.recyclerview.widget.ListAdapter
import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat import com.google.android.material.timepicker.TimeFormat
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.SettingsNavigationDirections import org.yuzu.yuzu_emu.SettingsNavigationDirections
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.view.* import org.yuzu.yuzu_emu.features.settings.model.view.*
import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* import org.yuzu.yuzu_emu.features.settings.ui.viewholder.*
import org.yuzu.yuzu_emu.utils.ParamPackage import org.yuzu.yuzu_emu.fragments.SettingsDialogFragment
import org.yuzu.yuzu_emu.model.SettingsViewModel
class SettingsAdapter( class SettingsAdapter(
private val fragment: Fragment, private val fragment: Fragment,
@ -42,6 +41,19 @@ class SettingsAdapter(
private val settingsViewModel: SettingsViewModel private val settingsViewModel: SettingsViewModel
get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java] get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java]
init {
fragment.viewLifecycleOwner.lifecycleScope.launch {
fragment.repeatOnLifecycle(Lifecycle.State.STARTED) {
settingsViewModel.adapterItemChanged.collect {
if (it != -1) {
notifyItemChanged(it)
settingsViewModel.setAdapterItemChanged(-1)
}
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (viewType) { return when (viewType) {
@ -73,23 +85,8 @@ class SettingsAdapter(
RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this) RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this)
} }
SettingsItem.TYPE_INPUT -> {
InputViewHolder(ListItemSettingInputBinding.inflate(inflater), this)
}
SettingsItem.TYPE_INT_SINGLE_CHOICE -> {
SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_INPUT_PROFILE -> {
InputProfileViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_STRING_INPUT -> {
StringInputViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
else -> { else -> {
// TODO: Create an error view since we can't return null now
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this) HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
} }
} }
@ -129,15 +126,6 @@ class SettingsAdapter(
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
} }
fun onIntSingleChoiceClick(item: IntSingleChoiceSetting, position: Int) {
SettingsDialogFragment.newInstance(
settingsViewModel,
item,
SettingsItem.TYPE_INT_SINGLE_CHOICE,
position
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
}
fun onDateTimeClick(item: DateTimeSetting, position: Int) { fun onDateTimeClick(item: DateTimeSetting, position: Int) {
val storedTime = item.getValue() * 1000 val storedTime = item.getValue() * 1000
@ -197,214 +185,6 @@ class SettingsAdapter(
fragment.view?.findNavController()?.navigate(action) fragment.view?.findNavController()?.navigate(action)
} }
fun onInputProfileClick(item: InputProfileSetting, position: Int) {
InputProfileDialogFragment.newInstance(
settingsViewModel,
item,
position
).show(fragment.childFragmentManager, InputProfileDialogFragment.TAG)
}
fun onInputClick(item: InputSetting, position: Int) {
InputDialogFragment.newInstance(
settingsViewModel,
item,
position
).show(fragment.childFragmentManager, InputDialogFragment.TAG)
}
fun onInputOptionsClick(anchor: View, item: InputSetting, position: Int) {
val popup = PopupMenu(context, anchor)
popup.menuInflater.inflate(R.menu.menu_input_options, popup.menu)
popup.menu.apply {
val invertAxis = findItem(R.id.invert_axis)
val invertButton = findItem(R.id.invert_button)
val toggleButton = findItem(R.id.toggle_button)
val turboButton = findItem(R.id.turbo_button)
val setThreshold = findItem(R.id.set_threshold)
val toggleAxis = findItem(R.id.toggle_axis)
when (item) {
is AnalogInputSetting -> {
val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
invertAxis.isVisible = true
invertAxis.isCheckable = true
invertAxis.isChecked = when (item.analogDirection) {
AnalogDirection.Left, AnalogDirection.Right -> {
params.get("invert_x", "+") == "-"
}
AnalogDirection.Up, AnalogDirection.Down -> {
params.get("invert_y", "+") == "-"
}
}
invertAxis.setOnMenuItemClickListener {
if (item.analogDirection == AnalogDirection.Left ||
item.analogDirection == AnalogDirection.Right
) {
val invertValue = params.get("invert_x", "+") == "-"
val invertString = if (invertValue) "+" else "-"
params.set("invert_x", invertString)
} else if (
item.analogDirection == AnalogDirection.Up ||
item.analogDirection == AnalogDirection.Down
) {
val invertValue = params.get("invert_y", "+") == "-"
val invertString = if (invertValue) "+" else "-"
params.set("invert_y", invertString)
}
true
}
popup.setOnDismissListener {
NativeInput.setStickParam(item.playerIndex, item.nativeAnalog, params)
settingsViewModel.setDatasetChanged(true)
}
}
is ButtonInputSetting -> {
val params = NativeInput.getButtonParam(item.playerIndex, item.nativeButton)
if (params.has("code") || params.has("button") || params.has("hat")) {
val buttonInvert = params.get("inverted", false)
invertButton.isVisible = true
invertButton.isCheckable = true
invertButton.isChecked = buttonInvert
invertButton.setOnMenuItemClickListener {
params.set("inverted", !buttonInvert)
true
}
val toggle = params.get("toggle", false)
toggleButton.isVisible = true
toggleButton.isCheckable = true
toggleButton.isChecked = toggle
toggleButton.setOnMenuItemClickListener {
params.set("toggle", !toggle)
true
}
val turbo = params.get("turbo", false)
turboButton.isVisible = true
turboButton.isCheckable = true
turboButton.isChecked = turbo
turboButton.setOnMenuItemClickListener {
params.set("turbo", !turbo)
true
}
} else if (params.has("axis")) {
val axisInvert = params.get("invert", "+") == "-"
invertAxis.isVisible = true
invertAxis.isCheckable = true
invertAxis.isChecked = axisInvert
invertAxis.setOnMenuItemClickListener {
params.set("invert", if (!axisInvert) "-" else "+")
true
}
val buttonInvert = params.get("inverted", false)
invertButton.isVisible = true
invertButton.isCheckable = true
invertButton.isChecked = buttonInvert
invertButton.setOnMenuItemClickListener {
params.set("inverted", !buttonInvert)
true
}
setThreshold.isVisible = true
val thresholdSetting = object : AbstractIntSetting {
override val key = ""
override fun getInt(needsGlobal: Boolean): Int =
(params.get("threshold", 0.5f) * 100).toInt()
override fun setInt(value: Int) {
params.set("threshold", value.toFloat() / 100)
NativeInput.setButtonParam(
item.playerIndex,
item.nativeButton,
params
)
}
override val defaultValue = 50
override fun getValueAsString(needsGlobal: Boolean): String =
getInt(needsGlobal).toString()
override fun reset() = setInt(defaultValue)
}
setThreshold.setOnMenuItemClickListener {
onSliderClick(
SliderSetting(thresholdSetting, R.string.set_threshold),
position
)
true
}
val axisToggle = params.get("toggle", false)
toggleAxis.isVisible = true
toggleAxis.isCheckable = true
toggleAxis.isChecked = axisToggle
toggleAxis.setOnMenuItemClickListener {
params.set("toggle", !axisToggle)
true
}
}
popup.setOnDismissListener {
NativeInput.setButtonParam(item.playerIndex, item.nativeButton, params)
settingsViewModel.setAdapterItemChanged(position)
}
}
is ModifierInputSetting -> {
val stickParams = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
val modifierParams = ParamPackage(stickParams.get("modifier", ""))
val invert = modifierParams.get("inverted", false)
invertButton.isVisible = true
invertButton.isCheckable = true
invertButton.isChecked = invert
invertButton.setOnMenuItemClickListener {
modifierParams.set("inverted", !invert)
stickParams.set("modifier", modifierParams.serialize())
true
}
val toggle = modifierParams.get("toggle", false)
toggleButton.isVisible = true
toggleButton.isCheckable = true
toggleButton.isChecked = toggle
toggleButton.setOnMenuItemClickListener {
modifierParams.set("toggle", !toggle)
stickParams.set("modifier", modifierParams.serialize())
true
}
popup.setOnDismissListener {
NativeInput.setStickParam(
item.playerIndex,
item.nativeAnalog,
stickParams
)
settingsViewModel.setAdapterItemChanged(position)
}
}
}
}
popup.show()
}
fun onStringInputClick(item: StringInputSetting, position: Int) {
SettingsDialogFragment.newInstance(
settingsViewModel,
item,
SettingsItem.TYPE_STRING_INPUT,
position
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
}
fun onLongClick(item: SettingsItem, position: Int): Boolean { fun onLongClick(item: SettingsItem, position: Int): Boolean {
SettingsDialogFragment.newInstance( SettingsDialogFragment.newInstance(
settingsViewModel, settingsViewModel,

View File

@ -8,22 +8,25 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.model.SettingsViewModel
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
class SettingsFragment : Fragment() { class SettingsFragment : Fragment() {
private lateinit var presenter: SettingsFragmentPresenter private lateinit var presenter: SettingsFragmentPresenter
@ -42,12 +45,6 @@ class SettingsFragment : Fragment() {
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
val playerIndex = getPlayerIndex()
if (playerIndex != -1) {
NativeInput.loadInputProfiles()
NativeInput.reloadInputDevices()
}
} }
override fun onCreateView( override fun onCreateView(
@ -59,9 +56,9 @@ class SettingsFragment : Fragment() {
return binding.root return binding.root
} }
@SuppressLint("NotifyDataSetChanged") // This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsAdapter = SettingsAdapter(this, requireContext()) settingsAdapter = SettingsAdapter(this, requireContext())
presenter = SettingsFragmentPresenter( presenter = SettingsFragmentPresenter(
settingsViewModel, settingsViewModel,
@ -74,17 +71,7 @@ class SettingsFragment : Fragment() {
) { ) {
args.game!!.title args.game!!.title
} else { } else {
when (args.menuTag) { getString(args.menuTag.titleId)
Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> Settings.getPlayerString(1)
Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> Settings.getPlayerString(2)
Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> Settings.getPlayerString(3)
Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> Settings.getPlayerString(4)
Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> Settings.getPlayerString(5)
Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> Settings.getPlayerString(6)
Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> Settings.getPlayerString(7)
Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> Settings.getPlayerString(8)
else -> getString(args.menuTag.titleId)
}
} }
binding.listSettings.apply { binding.listSettings.apply {
adapter = settingsAdapter adapter = settingsAdapter
@ -95,37 +82,16 @@ class SettingsFragment : Fragment() {
settingsViewModel.setShouldNavigateBack(true) settingsViewModel.setShouldNavigateBack(true)
} }
settingsViewModel.shouldReloadSettingsList.collect( viewLifecycleOwner.lifecycleScope.apply {
viewLifecycleOwner, launch {
resetState = { settingsViewModel.setShouldReloadSettingsList(false) } repeatOnLifecycle(Lifecycle.State.CREATED) {
) { if (it) presenter.loadSettingsList() } settingsViewModel.shouldReloadSettingsList.collectLatest {
settingsViewModel.adapterItemChanged.collect( if (it) {
viewLifecycleOwner, settingsViewModel.setShouldReloadSettingsList(false)
resetState = { settingsViewModel.setAdapterItemChanged(-1) } presenter.loadSettingsList()
) { if (it != -1) settingsAdapter?.notifyItemChanged(it) } }
settingsViewModel.datasetChanged.collect( }
viewLifecycleOwner, }
resetState = { settingsViewModel.setDatasetChanged(false) }
) { if (it) settingsAdapter?.notifyDataSetChanged() }
settingsViewModel.reloadListAndNotifyDataset.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setReloadListAndNotifyDataset(false) }
) { if (it) presenter.loadSettingsList(true) }
settingsViewModel.shouldShowResetInputDialog.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setShouldShowResetInputDialog(false) }
) {
if (it) {
MessageDialogFragment.newInstance(
activity = requireActivity(),
titleId = R.string.reset_mapping,
descriptionId = R.string.reset_mapping_description,
positiveAction = {
NativeInput.resetControllerMappings(getPlayerIndex())
settingsViewModel.setReloadListAndNotifyDataset(true)
},
negativeAction = {}
).show(parentFragmentManager, MessageDialogFragment.TAG)
} }
} }
@ -149,19 +115,6 @@ class SettingsFragment : Fragment() {
setInsets() setInsets()
} }
private fun getPlayerIndex(): Int =
when (args.menuTag) {
Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> 0
Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> 1
Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> 2
Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> 3
Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> 4
Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> 5
Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> 6
Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> 7
else -> -1
}
private fun setInsets() { private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener( ViewCompat.setOnApplyWindowInsetsListener(
binding.root binding.root
@ -172,10 +125,18 @@ class SettingsFragment : Fragment() {
val leftInsets = barInsets.left + cutoutInsets.left val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right val rightInsets = barInsets.right + cutoutInsets.right
binding.listSettings.updateMargins(left = leftInsets, right = rightInsets) val mlpSettingsList = binding.listSettings.layoutParams as MarginLayoutParams
binding.listSettings.updatePadding(bottom = barInsets.bottom) mlpSettingsList.leftMargin = leftInsets
mlpSettingsList.rightMargin = rightInsets
binding.listSettings.layoutParams = mlpSettingsList
binding.listSettings.updatePadding(
bottom = barInsets.bottom
)
binding.appbarSettings.updateMargins(left = leftInsets, right = rightInsets) val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams
mlpAppBar.leftMargin = leftInsets
mlpAppBar.rightMargin = rightInsets
binding.appbarSettings.layoutParams = mlpAppBar
windowInsets windowInsets
} }
} }

View File

@ -3,17 +3,11 @@
package org.yuzu.yuzu_emu.features.settings.ui package org.yuzu.yuzu_emu.features.settings.ui
import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
import org.yuzu.yuzu_emu.features.input.model.NativeButton
import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
@ -21,22 +15,18 @@ import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.LongSetting import org.yuzu.yuzu_emu.features.settings.model.LongSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.Settings.MenuTag
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.features.settings.model.view.* import org.yuzu.yuzu_emu.features.settings.model.view.*
import org.yuzu.yuzu_emu.utils.InputHandler import org.yuzu.yuzu_emu.model.SettingsViewModel
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
class SettingsFragmentPresenter( class SettingsFragmentPresenter(
private val settingsViewModel: SettingsViewModel, private val settingsViewModel: SettingsViewModel,
private val adapter: SettingsAdapter, private val adapter: SettingsAdapter,
private var menuTag: MenuTag private var menuTag: Settings.MenuTag
) { ) {
private var settingsList = ArrayList<SettingsItem>() private var settingsList = ArrayList<SettingsItem>()
private val context get() = YuzuApplication.appContext
// Extension for altering settings list based on each setting's properties // Extension for altering settings list based on each setting's properties
fun ArrayList<SettingsItem>.add(key: String) { fun ArrayList<SettingsItem>.add(key: String) {
val item = SettingsItem.settingsItems[key]!! val item = SettingsItem.settingsItems[key]!!
@ -63,90 +53,73 @@ class SettingsFragmentPresenter(
add(item) add(item)
} }
// Allows you to show/hide abstract settings based on the paired setting key
fun ArrayList<SettingsItem>.addAbstract(item: SettingsItem) {
val pairedSettingKey = item.setting.pairedSettingKey
if (pairedSettingKey.isNotEmpty()) {
val pairedSettingsItem =
this.firstOrNull { it.setting.key == pairedSettingKey } ?: return
val pairedSetting = pairedSettingsItem.setting as AbstractBooleanSetting
if (!pairedSetting.getBoolean(!NativeConfig.isPerGameConfigLoaded())) return
}
add(item)
}
fun onViewCreated() { fun onViewCreated() {
loadSettingsList() loadSettingsList()
} }
@SuppressLint("NotifyDataSetChanged") fun loadSettingsList() {
fun loadSettingsList(notifyDataSetChanged: Boolean = false) {
val sl = ArrayList<SettingsItem>() val sl = ArrayList<SettingsItem>()
when (menuTag) { when (menuTag) {
MenuTag.SECTION_ROOT -> addConfigSettings(sl) Settings.MenuTag.SECTION_ROOT -> addConfigSettings(sl)
MenuTag.SECTION_SYSTEM -> addSystemSettings(sl) Settings.MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl) Settings.MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
MenuTag.SECTION_AUDIO -> addAudioSettings(sl) Settings.MenuTag.SECTION_AUDIO -> addAudioSettings(sl)
MenuTag.SECTION_INPUT -> addInputSettings(sl) Settings.MenuTag.SECTION_THEME -> addThemeSettings(sl)
MenuTag.SECTION_INPUT_PLAYER_ONE -> addInputPlayer(sl, 0) Settings.MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
MenuTag.SECTION_INPUT_PLAYER_TWO -> addInputPlayer(sl, 1) else -> {
MenuTag.SECTION_INPUT_PLAYER_THREE -> addInputPlayer(sl, 2) val context = YuzuApplication.appContext
MenuTag.SECTION_INPUT_PLAYER_FOUR -> addInputPlayer(sl, 3) Toast.makeText(
MenuTag.SECTION_INPUT_PLAYER_FIVE -> addInputPlayer(sl, 4) context,
MenuTag.SECTION_INPUT_PLAYER_SIX -> addInputPlayer(sl, 5) context.getString(R.string.unimplemented_menu),
MenuTag.SECTION_INPUT_PLAYER_SEVEN -> addInputPlayer(sl, 6) Toast.LENGTH_SHORT
MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7) ).show()
MenuTag.SECTION_THEME -> addThemeSettings(sl) return
MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
}
settingsList = sl
adapter.submitList(settingsList) {
if (notifyDataSetChanged) {
adapter.notifyDataSetChanged()
} }
} }
settingsList = sl
adapter.submitList(settingsList)
} }
private fun addConfigSettings(sl: ArrayList<SettingsItem>) { private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
sl.apply { sl.apply {
add( add(
SubmenuSetting( SubmenuSetting(
titleId = R.string.preferences_system, R.string.preferences_system,
descriptionId = R.string.preferences_system_description, R.string.preferences_system_description,
iconId = R.drawable.ic_system_settings, R.drawable.ic_system_settings,
menuKey = MenuTag.SECTION_SYSTEM Settings.MenuTag.SECTION_SYSTEM
) )
) )
add( add(
SubmenuSetting( SubmenuSetting(
titleId = R.string.preferences_graphics, R.string.preferences_graphics,
descriptionId = R.string.preferences_graphics_description, R.string.preferences_graphics_description,
iconId = R.drawable.ic_graphics, R.drawable.ic_graphics,
menuKey = MenuTag.SECTION_RENDERER Settings.MenuTag.SECTION_RENDERER
) )
) )
add( add(
SubmenuSetting( SubmenuSetting(
titleId = R.string.preferences_audio, R.string.preferences_audio,
descriptionId = R.string.preferences_audio_description, R.string.preferences_audio_description,
iconId = R.drawable.ic_audio, R.drawable.ic_audio,
menuKey = MenuTag.SECTION_AUDIO Settings.MenuTag.SECTION_AUDIO
) )
) )
add( add(
SubmenuSetting( SubmenuSetting(
titleId = R.string.preferences_debug, R.string.preferences_debug,
descriptionId = R.string.preferences_debug_description, R.string.preferences_debug_description,
iconId = R.drawable.ic_code, R.drawable.ic_code,
menuKey = MenuTag.SECTION_DEBUG Settings.MenuTag.SECTION_DEBUG
) )
) )
add( add(
RunnableSetting( RunnableSetting(
titleId = R.string.reset_to_default, R.string.reset_to_default,
descriptionId = R.string.reset_to_default_description, R.string.reset_to_default_description,
isRunnable = !NativeLibrary.isRunning(), false,
iconId = R.drawable.ic_restore R.drawable.ic_restore
) { settingsViewModel.setShouldShowResetSettingsDialog(true) } ) { settingsViewModel.setShouldShowResetSettingsDialog(true) }
) )
} }
@ -154,7 +127,6 @@ class SettingsFragmentPresenter(
private fun addSystemSettings(sl: ArrayList<SettingsItem>) { private fun addSystemSettings(sl: ArrayList<SettingsItem>) {
sl.apply { sl.apply {
add(StringSetting.DEVICE_NAME.key)
add(BooleanSetting.RENDERER_USE_SPEED_LIMIT.key) add(BooleanSetting.RENDERER_USE_SPEED_LIMIT.key)
add(ShortSetting.RENDERER_SPEED_LIMIT.key) add(ShortSetting.RENDERER_SPEED_LIMIT.key)
add(BooleanSetting.USE_DOCKED_MODE.key) add(BooleanSetting.USE_DOCKED_MODE.key)
@ -171,12 +143,10 @@ class SettingsFragmentPresenter(
add(IntSetting.RENDERER_RESOLUTION.key) add(IntSetting.RENDERER_RESOLUTION.key)
add(IntSetting.RENDERER_VSYNC.key) add(IntSetting.RENDERER_VSYNC.key)
add(IntSetting.RENDERER_SCALING_FILTER.key) add(IntSetting.RENDERER_SCALING_FILTER.key)
add(IntSetting.FSR_SHARPENING_SLIDER.key)
add(IntSetting.RENDERER_ANTI_ALIASING.key) add(IntSetting.RENDERER_ANTI_ALIASING.key)
add(IntSetting.MAX_ANISOTROPY.key) add(IntSetting.MAX_ANISOTROPY.key)
add(IntSetting.RENDERER_SCREEN_LAYOUT.key) add(IntSetting.RENDERER_SCREEN_LAYOUT.key)
add(IntSetting.RENDERER_ASPECT_RATIO.key) add(IntSetting.RENDERER_ASPECT_RATIO.key)
add(IntSetting.VERTICAL_ALIGNMENT.key)
add(BooleanSetting.PICTURE_IN_PICTURE.key) add(BooleanSetting.PICTURE_IN_PICTURE.key)
add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key) add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key)
add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key) add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key)
@ -192,671 +162,6 @@ class SettingsFragmentPresenter(
} }
} }
private fun addInputSettings(sl: ArrayList<SettingsItem>) {
settingsViewModel.currentDevice = 0
if (NativeConfig.isPerGameConfigLoaded()) {
NativeInput.loadInputProfiles()
val profiles = NativeInput.getInputProfileNames().toMutableList()
profiles.add(0, "")
val prettyProfiles = profiles.toTypedArray()
prettyProfiles[0] =
context.getString(R.string.use_global_input_configuration)
sl.apply {
for (i in 0 until 8) {
add(
IntSingleChoiceSetting(
getPerGameProfileSetting(profiles, i),
titleString = getPlayerProfileString(i + 1),
choices = prettyProfiles,
values = IntArray(profiles.size) { it }.toTypedArray()
)
)
}
}
return
}
val getConnectedIcon: (Int) -> Int = { playerIndex: Int ->
if (NativeInput.getIsConnected(playerIndex)) {
R.drawable.ic_controller
} else {
R.drawable.ic_controller_disconnected
}
}
val inputSettings = NativeConfig.getInputSettings(true)
sl.apply {
add(
SubmenuSetting(
titleString = Settings.getPlayerString(1),
descriptionString = inputSettings[0].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_ONE,
iconId = getConnectedIcon(0)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(2),
descriptionString = inputSettings[1].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_TWO,
iconId = getConnectedIcon(1)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(3),
descriptionString = inputSettings[2].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_THREE,
iconId = getConnectedIcon(2)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(4),
descriptionString = inputSettings[3].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_FOUR,
iconId = getConnectedIcon(3)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(5),
descriptionString = inputSettings[4].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_FIVE,
iconId = getConnectedIcon(4)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(6),
descriptionString = inputSettings[5].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_SIX,
iconId = getConnectedIcon(5)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(7),
descriptionString = inputSettings[6].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_SEVEN,
iconId = getConnectedIcon(6)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(8),
descriptionString = inputSettings[7].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_EIGHT,
iconId = getConnectedIcon(7)
)
)
}
}
private fun getPlayerProfileString(player: Int): String =
context.getString(R.string.player_num_profile, player)
private fun getPerGameProfileSetting(
profiles: List<String>,
playerIndex: Int
): AbstractIntSetting {
return object : AbstractIntSetting {
private val players
get() = NativeConfig.getInputSettings(false)
override val key = ""
override fun getInt(needsGlobal: Boolean): Int {
val currentProfile = players[playerIndex].profileName
profiles.forEachIndexed { i, profile ->
if (profile == currentProfile) {
return i
}
}
return 0
}
override fun setInt(value: Int) {
NativeInput.loadPerGameConfiguration(playerIndex, value, profiles[value])
NativeInput.connectControllers(playerIndex)
NativeConfig.saveControlPlayerValues()
}
override val defaultValue = 0
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
override fun reset() = setInt(defaultValue)
override var global = true
override val isRuntimeModifiable = true
override val isSaveable = true
}
}
private fun addInputPlayer(sl: ArrayList<SettingsItem>, playerIndex: Int) {
sl.apply {
val connectedSetting = object : AbstractBooleanSetting {
override val key = "connected"
override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeInput.getIsConnected(playerIndex)
override fun setBoolean(value: Boolean) =
NativeInput.connectControllers(playerIndex, value)
override val defaultValue = playerIndex == 0
override fun getValueAsString(needsGlobal: Boolean): String =
getBoolean(needsGlobal).toString()
override fun reset() = setBoolean(defaultValue)
}
add(SwitchSetting(connectedSetting, R.string.connected))
val styleTags = NativeInput.getSupportedStyleTags(playerIndex)
val npadType = object : AbstractIntSetting {
override val key = "npad_type"
override fun getInt(needsGlobal: Boolean): Int {
val styleIndex = NativeInput.getStyleIndex(playerIndex)
return styleTags.indexOfFirst { it == styleIndex }
}
override fun setInt(value: Int) {
NativeInput.setStyleIndex(playerIndex, styleTags[value])
settingsViewModel.setReloadListAndNotifyDataset(true)
}
override val defaultValue = NpadStyleIndex.Fullkey.int
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
override fun reset() = setInt(defaultValue)
override val pairedSettingKey: String = "connected"
}
addAbstract(
IntSingleChoiceSetting(
npadType,
titleId = R.string.controller_type,
choices = styleTags.map { context.getString(it.nameId) }
.toTypedArray(),
values = IntArray(styleTags.size) { it }.toTypedArray()
)
)
InputHandler.updateControllerData()
val autoMappingSetting = object : AbstractIntSetting {
override val key = "auto_mapping_device"
override fun getInt(needsGlobal: Boolean): Int = -1
override fun setInt(value: Int) {
val registeredController = InputHandler.registeredControllers[value + 1]
val displayName = registeredController.get(
"display",
context.getString(R.string.unknown)
)
NativeInput.updateMappingsWithDefault(
playerIndex,
registeredController,
displayName
)
Toast.makeText(
context,
context.getString(R.string.attempted_auto_map, displayName),
Toast.LENGTH_SHORT
).show()
settingsViewModel.setReloadListAndNotifyDataset(true)
}
override val defaultValue = -1
override fun getValueAsString(needsGlobal: Boolean) = getInt().toString()
override fun reset() = setInt(defaultValue)
override val isRuntimeModifiable: Boolean = true
}
val unknownString = context.getString(R.string.unknown)
val prettyAutoMappingControllerList = InputHandler.registeredControllers.mapNotNull {
val port = it.get("port", -1)
return@mapNotNull if (port == 100 || port == -1) {
null
} else {
it.get("display", unknownString)
}
}.toTypedArray()
add(
IntSingleChoiceSetting(
autoMappingSetting,
titleId = R.string.auto_map,
descriptionId = R.string.auto_map_description,
choices = prettyAutoMappingControllerList,
values = IntArray(prettyAutoMappingControllerList.size) { it }.toTypedArray()
)
)
val mappingFilterSetting = object : AbstractIntSetting {
override val key = "mapping_filter"
override fun getInt(needsGlobal: Boolean): Int = settingsViewModel.currentDevice
override fun setInt(value: Int) {
settingsViewModel.currentDevice = value
}
override val defaultValue = 0
override fun getValueAsString(needsGlobal: Boolean) = getInt().toString()
override fun reset() = setInt(defaultValue)
override val isRuntimeModifiable: Boolean = true
}
val prettyControllerList = InputHandler.registeredControllers.mapNotNull {
return@mapNotNull if (it.get("port", 0) == 100) {
null
} else {
it.get("display", unknownString)
}
}.toTypedArray()
add(
IntSingleChoiceSetting(
mappingFilterSetting,
titleId = R.string.input_mapping_filter,
descriptionId = R.string.input_mapping_filter_description,
choices = prettyControllerList,
values = IntArray(prettyControllerList.size) { it }.toTypedArray()
)
)
add(InputProfileSetting(playerIndex))
add(
RunnableSetting(titleId = R.string.reset_to_default, isRunnable = true) {
settingsViewModel.setShouldShowResetInputDialog(true)
}
)
val styleIndex = NativeInput.getStyleIndex(playerIndex)
// Buttons
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld,
NpadStyleIndex.JoyconDual -> {
add(HeaderSetting(R.string.buttons))
add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus))
add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus))
add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home))
add(
ButtonInputSetting(
playerIndex,
NativeButton.Capture,
R.string.button_capture
)
)
}
NpadStyleIndex.JoyconLeft -> {
add(HeaderSetting(R.string.buttons))
add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus))
add(
ButtonInputSetting(
playerIndex,
NativeButton.Capture,
R.string.button_capture
)
)
}
NpadStyleIndex.JoyconRight -> {
add(HeaderSetting(R.string.buttons))
add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus))
add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home))
}
NpadStyleIndex.GameCube -> {
add(HeaderSetting(R.string.buttons))
add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.start_pause))
}
else -> {
// No-op
}
}
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld,
NpadStyleIndex.JoyconDual,
NpadStyleIndex.JoyconLeft -> {
add(HeaderSetting(R.string.dpad))
add(ButtonInputSetting(playerIndex, NativeButton.DUp, R.string.up))
add(ButtonInputSetting(playerIndex, NativeButton.DDown, R.string.down))
add(ButtonInputSetting(playerIndex, NativeButton.DLeft, R.string.left))
add(ButtonInputSetting(playerIndex, NativeButton.DRight, R.string.right))
}
else -> {
// No-op
}
}
// Left stick
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld,
NpadStyleIndex.JoyconDual,
NpadStyleIndex.JoyconLeft -> {
add(HeaderSetting(R.string.left_stick))
addAll(getStickDirections(playerIndex, NativeAnalog.LStick))
add(ButtonInputSetting(playerIndex, NativeButton.LStick, R.string.pressed))
addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick))
}
NpadStyleIndex.GameCube -> {
add(HeaderSetting(R.string.control_stick))
addAll(getStickDirections(playerIndex, NativeAnalog.LStick))
addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick))
}
else -> {
// No-op
}
}
// Right stick
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld,
NpadStyleIndex.JoyconDual,
NpadStyleIndex.JoyconRight -> {
add(HeaderSetting(R.string.right_stick))
addAll(getStickDirections(playerIndex, NativeAnalog.RStick))
add(ButtonInputSetting(playerIndex, NativeButton.RStick, R.string.pressed))
addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick))
}
NpadStyleIndex.GameCube -> {
add(HeaderSetting(R.string.c_stick))
addAll(getStickDirections(playerIndex, NativeAnalog.RStick))
addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick))
}
else -> {
// No-op
}
}
// L/R, ZL/ZR, and SL/SR
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
}
NpadStyleIndex.JoyconDual -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
add(
ButtonInputSetting(
playerIndex,
NativeButton.SLLeft,
R.string.button_sl_left
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SRLeft,
R.string.button_sr_left
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SLRight,
R.string.button_sl_right
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SRRight,
R.string.button_sr_right
)
)
}
NpadStyleIndex.JoyconLeft -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
add(
ButtonInputSetting(
playerIndex,
NativeButton.SLLeft,
R.string.button_sl_left
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SRLeft,
R.string.button_sr_left
)
)
}
NpadStyleIndex.JoyconRight -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
add(
ButtonInputSetting(
playerIndex,
NativeButton.SLRight,
R.string.button_sl_right
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SRRight,
R.string.button_sr_right
)
)
}
NpadStyleIndex.GameCube -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_z))
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_l))
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_r))
}
else -> {
// No-op
}
}
add(HeaderSetting(R.string.vibration))
val vibrationEnabledSetting = object : AbstractBooleanSetting {
override val key = "vibration"
override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeConfig.getInputSettings(true)[playerIndex].vibrationEnabled
override fun setBoolean(value: Boolean) {
val settings = NativeConfig.getInputSettings(true)
settings[playerIndex].vibrationEnabled = value
NativeConfig.setInputSettings(settings, true)
}
override val defaultValue = true
override fun getValueAsString(needsGlobal: Boolean): String =
getBoolean(needsGlobal).toString()
override fun reset() = setBoolean(defaultValue)
}
add(SwitchSetting(vibrationEnabledSetting, R.string.vibration))
val useSystemVibratorSetting = object : AbstractBooleanSetting {
override val key = ""
override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeConfig.getInputSettings(true)[playerIndex].useSystemVibrator
override fun setBoolean(value: Boolean) {
val settings = NativeConfig.getInputSettings(true)
settings[playerIndex].useSystemVibrator = value
NativeConfig.setInputSettings(settings, true)
}
override val defaultValue = playerIndex == 0
override fun getValueAsString(needsGlobal: Boolean): String =
getBoolean(needsGlobal).toString()
override fun reset() = setBoolean(defaultValue)
override val pairedSettingKey: String = "vibration"
}
addAbstract(SwitchSetting(useSystemVibratorSetting, R.string.use_system_vibrator))
val vibrationStrengthSetting = object : AbstractIntSetting {
override val key = ""
override fun getInt(needsGlobal: Boolean): Int =
NativeConfig.getInputSettings(true)[playerIndex].vibrationStrength
override fun setInt(value: Int) {
val settings = NativeConfig.getInputSettings(true)
settings[playerIndex].vibrationStrength = value
NativeConfig.setInputSettings(settings, true)
}
override val defaultValue = 100
override fun getValueAsString(needsGlobal: Boolean): String =
getInt(needsGlobal).toString()
override fun reset() = setInt(defaultValue)
override val pairedSettingKey: String = "vibration"
}
addAbstract(
SliderSetting(vibrationStrengthSetting, R.string.vibration_strength, units = "%")
)
}
}
// Convenience function for creating AbstractIntSettings for modifier range/stick range/stick deadzones
private fun getStickIntSettingFromParam(
playerIndex: Int,
paramName: String,
stick: NativeAnalog,
defaultValue: Float
): AbstractIntSetting =
object : AbstractIntSetting {
val params get() = NativeInput.getStickParam(playerIndex, stick)
override val key = ""
override fun getInt(needsGlobal: Boolean): Int =
(params.get(paramName, defaultValue) * 100).toInt()
override fun setInt(value: Int) {
val tempParams = params
tempParams.set(paramName, value.toFloat() / 100)
NativeInput.setStickParam(playerIndex, stick, tempParams)
}
override val defaultValue = (defaultValue * 100).toInt()
override fun getValueAsString(needsGlobal: Boolean): String =
getInt(needsGlobal).toString()
override fun reset() = setInt(this.defaultValue)
}
private fun getExtraStickSettings(
playerIndex: Int,
nativeAnalog: NativeAnalog
): List<SettingsItem> {
val stickIsController =
NativeInput.isController(NativeInput.getStickParam(playerIndex, nativeAnalog))
val modifierRangeSetting =
getStickIntSettingFromParam(playerIndex, "modifier_scale", nativeAnalog, 0.5f)
val stickRangeSetting =
getStickIntSettingFromParam(playerIndex, "range", nativeAnalog, 0.95f)
val stickDeadzoneSetting =
getStickIntSettingFromParam(playerIndex, "deadzone", nativeAnalog, 0.15f)
val out = mutableListOf<SettingsItem>().apply {
if (stickIsController) {
add(SliderSetting(stickRangeSetting, titleId = R.string.range, min = 25, max = 150))
add(SliderSetting(stickDeadzoneSetting, R.string.deadzone))
} else {
add(ModifierInputSetting(playerIndex, NativeAnalog.LStick, R.string.modifier))
add(SliderSetting(modifierRangeSetting, R.string.modifier_range))
}
}
return out
}
private fun getStickDirections(player: Int, stick: NativeAnalog): List<AnalogInputSetting> =
listOf(
AnalogInputSetting(
player,
stick,
AnalogDirection.Up,
R.string.up
),
AnalogInputSetting(
player,
stick,
AnalogDirection.Down,
R.string.down
),
AnalogInputSetting(
player,
stick,
AnalogDirection.Left,
R.string.left
),
AnalogInputSetting(
player,
stick,
AnalogDirection.Right,
R.string.right
)
)
private fun addThemeSettings(sl: ArrayList<SettingsItem>) { private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
sl.apply { sl.apply {
val theme: AbstractIntSetting = object : AbstractIntSetting { val theme: AbstractIntSetting = object : AbstractIntSetting {
@ -879,18 +184,20 @@ class SettingsFragmentPresenter(
add( add(
SingleChoiceSetting( SingleChoiceSetting(
theme, theme,
titleId = R.string.change_app_theme, R.string.change_app_theme,
choicesId = R.array.themeEntriesA12, 0,
valuesId = R.array.themeValuesA12 R.array.themeEntriesA12,
R.array.themeValuesA12
) )
) )
} else { } else {
add( add(
SingleChoiceSetting( SingleChoiceSetting(
theme, theme,
titleId = R.string.change_app_theme, R.string.change_app_theme,
choicesId = R.array.themeEntries, 0,
valuesId = R.array.themeValues R.array.themeEntries,
R.array.themeValues
) )
) )
} }
@ -919,9 +226,10 @@ class SettingsFragmentPresenter(
add( add(
SingleChoiceSetting( SingleChoiceSetting(
themeMode, themeMode,
titleId = R.string.change_theme_mode, R.string.change_theme_mode,
choicesId = R.array.themeModeEntries, 0,
valuesId = R.array.themeModeValues R.array.themeModeEntries,
R.array.themeModeValues
) )
) )
@ -952,8 +260,8 @@ class SettingsFragmentPresenter(
add( add(
SwitchSetting( SwitchSetting(
blackBackgrounds, blackBackgrounds,
titleId = R.string.use_black_backgrounds, R.string.use_black_backgrounds,
descriptionId = R.string.use_black_backgrounds_description R.string.use_black_backgrounds_description
) )
) )
} }

View File

@ -13,7 +13,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.utils.NativeConfig
class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) { SettingViewHolder(binding.root, adapter) {
@ -21,17 +21,28 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
override fun bind(item: SettingsItem) { override fun bind(item: SettingsItem) {
setting = item as DateTimeSetting setting = item as DateTimeSetting
binding.textSettingName.text = item.title binding.textSettingName.setText(item.nameId)
binding.textSettingDescription.setVisible(item.description.isNotEmpty()) if (item.descriptionId != 0) {
binding.textSettingDescription.text = item.description binding.textSettingDescription.setText(item.descriptionId)
binding.textSettingValue.setVisible(true) binding.textSettingDescription.visibility = View.VISIBLE
} else {
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.visibility = View.VISIBLE
val epochTime = setting.getValue() val epochTime = setting.getValue()
val instant = Instant.ofEpochMilli(epochTime * 1000) val instant = Instant.ofEpochMilli(epochTime * 1000)
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
binding.textSettingValue.text = dateFormatter.format(zonedTime) binding.textSettingValue.text = dateFormatter.format(zonedTime)
binding.buttonClear.setVisible(setting.clearable) binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setOnClickListener { binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition) adapter.onClearClick(setting, bindingAdapterPosition)
} }

View File

@ -16,7 +16,7 @@ class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: Sett
} }
override fun bind(item: SettingsItem) { override fun bind(item: SettingsItem) {
binding.textHeaderName.text = item.title binding.textHeaderName.setText(item.nameId)
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {

View File

@ -1,34 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui.viewholder
import android.view.View
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
class InputProfileViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
private lateinit var setting: InputProfileSetting
override fun bind(item: SettingsItem) {
setting = item as InputProfileSetting
binding.textSettingName.text = setting.title
binding.textSettingValue.text =
setting.getCurrentProfile().ifEmpty { binding.root.context.getString(R.string.not_set) }
binding.textSettingDescription.setVisible(false)
binding.buttonClear.setVisible(false)
binding.icon.setVisible(false)
binding.buttonClear.setVisible(false)
}
override fun onClick(clicked: View) =
adapter.onInputProfileClick(setting, bindingAdapterPosition)
override fun onLongClick(clicked: View): Boolean = false
}

View File

@ -1,60 +0,0 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui.viewholder
import android.view.View
import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
class InputViewHolder(val binding: ListItemSettingInputBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
private lateinit var setting: InputSetting
override fun bind(item: SettingsItem) {
setting = item as InputSetting
binding.textSettingName.text = setting.title
binding.textSettingValue.text = setting.getSelectedValue()
when (item) {
is AnalogInputSetting -> {
val param = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
binding.buttonOptions.setVisible(
param.get("engine", "") == "analog_from_button" ||
param.has("axis_x") || param.has("axis_y")
)
}
is ButtonInputSetting -> {
val param = NativeInput.getButtonParam(item.playerIndex, item.nativeButton)
binding.buttonOptions.setVisible(
param.has("code") || param.has("button") || param.has("hat") ||
param.has("axis")
)
}
is ModifierInputSetting -> {
val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
binding.buttonOptions.setVisible(params.has("modifier"))
}
}
binding.buttonOptions.setOnClickListener(null)
binding.buttonOptions.setOnClickListener {
adapter.onInputOptionsClick(binding.buttonOptions, setting, bindingAdapterPosition)
}
}
override fun onClick(clicked: View) =
adapter.onInputClick(setting, bindingAdapterPosition)
override fun onLongClick(clicked: View): Boolean =
adapter.onLongClick(setting, bindingAdapterPosition)
}

View File

@ -5,11 +5,11 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder
import android.view.View import android.view.View
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.RunnableSetting import org.yuzu.yuzu_emu.features.settings.model.view.RunnableSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) { SettingViewHolder(binding.root, adapter) {
@ -17,28 +17,34 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
override fun bind(item: SettingsItem) { override fun bind(item: SettingsItem) {
setting = item as RunnableSetting setting = item as RunnableSetting
binding.icon.setVisible(setting.iconId != 0) if (item.iconId != 0) {
if (setting.iconId != 0) { binding.icon.visibility = View.VISIBLE
binding.icon.setImageDrawable( binding.icon.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(
binding.icon.resources, binding.icon.resources,
setting.iconId, item.iconId,
binding.icon.context.theme binding.icon.context.theme
) )
) )
} else {
binding.icon.visibility = View.GONE
} }
binding.textSettingName.text = setting.title binding.textSettingName.setText(item.nameId)
binding.textSettingDescription.setVisible(setting.description.isNotEmpty()) if (item.descriptionId != 0) {
binding.textSettingDescription.text = item.description binding.textSettingDescription.setText(item.descriptionId)
binding.textSettingValue.setVisible(false) binding.textSettingDescription.visibility = View.VISIBLE
binding.buttonClear.setVisible(false) } else {
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.visibility = View.GONE
binding.buttonClear.visibility = View.GONE
setStyle(setting.isEditable, binding) setStyle(setting.isEditable, binding)
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
if (setting.isRunnable) { if (!setting.isRuntimeRunnable && !NativeLibrary.isRunning()) {
setting.runnable.invoke() setting.runnable.invoke()
} }
} }

View File

@ -5,12 +5,11 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder
import android.view.View import android.view.View
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.utils.NativeConfig
class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) { SettingViewHolder(binding.root, adapter) {
@ -18,36 +17,40 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
override fun bind(item: SettingsItem) { override fun bind(item: SettingsItem) {
setting = item setting = item
binding.textSettingName.text = setting.title binding.textSettingName.setText(item.nameId)
binding.textSettingDescription.setVisible(item.description.isNotEmpty()) if (item.descriptionId != 0) {
binding.textSettingDescription.text = item.description binding.textSettingDescription.setText(item.descriptionId)
binding.textSettingDescription.visibility = View.VISIBLE
} else {
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.setVisible(true) binding.textSettingValue.visibility = View.VISIBLE
when (item) { if (item is SingleChoiceSetting) {
is SingleChoiceSetting -> { val resMgr = binding.textSettingValue.context.resources
val resMgr = binding.textSettingValue.context.resources val values = resMgr.getIntArray(item.valuesId)
val values = resMgr.getIntArray(item.valuesId) for (i in values.indices) {
for (i in values.indices) { if (values[i] == item.getSelectedValue()) {
if (values[i] == item.getSelectedValue()) { binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i]
binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] break
break
}
} }
} }
} else if (item is StringSingleChoiceSetting) {
is StringSingleChoiceSetting -> { for (i in item.values.indices) {
binding.textSettingValue.text = item.getSelectedValue() if (item.values[i] == item.getSelectedValue()) {
} binding.textSettingValue.text = item.choices[i]
break
is IntSingleChoiceSetting -> { }
binding.textSettingValue.text = item.getChoiceAt(item.getSelectedValue())
} }
} }
if (binding.textSettingValue.text.isEmpty()) {
binding.textSettingValue.setVisible(false)
}
binding.buttonClear.setVisible(setting.clearable) binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setOnClickListener { binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition) adapter.onClearClick(setting, bindingAdapterPosition)
} }
@ -60,25 +63,16 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
return return
} }
when (setting) { if (setting is SingleChoiceSetting) {
is SingleChoiceSetting -> adapter.onSingleChoiceClick( adapter.onSingleChoiceClick(
setting as SingleChoiceSetting, (setting as SingleChoiceSetting),
bindingAdapterPosition
)
} else if (setting is StringSingleChoiceSetting) {
adapter.onStringSingleChoiceClick(
(setting as StringSingleChoiceSetting),
bindingAdapterPosition bindingAdapterPosition
) )
is StringSingleChoiceSetting -> {
adapter.onStringSingleChoiceClick(
setting as StringSingleChoiceSetting,
bindingAdapterPosition
)
}
is IntSingleChoiceSetting -> {
adapter.onIntSingleChoiceClick(
setting as IntSingleChoiceSetting,
bindingAdapterPosition
)
}
} }
} }

View File

@ -9,7 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.utils.NativeConfig
class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) { SettingViewHolder(binding.root, adapter) {
@ -17,17 +17,27 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
override fun bind(item: SettingsItem) { override fun bind(item: SettingsItem) {
setting = item as SliderSetting setting = item as SliderSetting
binding.textSettingName.text = setting.title binding.textSettingName.setText(item.nameId)
binding.textSettingDescription.setVisible(item.description.isNotEmpty()) if (item.descriptionId != 0) {
binding.textSettingDescription.text = setting.description binding.textSettingDescription.setText(item.descriptionId)
binding.textSettingValue.setVisible(true) binding.textSettingDescription.visibility = View.VISIBLE
} else {
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.visibility = View.VISIBLE
binding.textSettingValue.text = String.format( binding.textSettingValue.text = String.format(
binding.textSettingValue.context.getString(R.string.value_with_units), binding.textSettingValue.context.getString(R.string.value_with_units),
setting.getSelectedValue(), setting.getSelectedValue(),
setting.units setting.units
) )
binding.buttonClear.setVisible(setting.clearable) binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setOnClickListener { binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition) adapter.onClearClick(setting, bindingAdapterPosition)
} }

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