Compare commits

..

5 Commits

Author SHA1 Message Date
yuzubot
c4f33d99a4 Android 218 2024-01-29 01:00:37 +00:00
yuzubot
53908818e1 Merge yuzu-emu#12830 2024-01-29 01:00:37 +00:00
yuzubot
87523c5246 Merge yuzu-emu#12814 2024-01-29 01:00:37 +00:00
yuzubot
e82ab1b9e1 Merge yuzu-emu#12760 2024-01-29 01:00:37 +00:00
yuzubot
10c1ccc092 Merge yuzu-emu#12749 2024-01-29 01:00:37 +00:00
1094 changed files with 49183 additions and 87448 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");
// which label to check for changes
const CHANGE_LABEL_MAINLINE = 'android-merge';
const CHANGE_LABEL_EA = 'android-ea-merge';
const CHANGE_LABEL = 'android-merge';
// 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 BUILD_EA = process.env.BUILD_EA == 'true';
const MAINLINE_TAG = process.env.MAINLINE_TAG;
async function checkBaseChanges(github) {
// query the commit date of the latest commit on this branch
@ -43,7 +40,20 @@ async function checkBaseChanges(github) {
async function checkAndroidChanges(github) {
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++) {
let pull = pulls[i];
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 tagList = tags.repository.refs.nodes;
let lastTag = 'android-1';
for (let i = 0; i < tagList.length; ++i) {
if (tagList[i].name.includes('android-')) {
lastTag = tagList[i].name;
break;
}
}
const lastTag = tagList[0] ? tagList[0].name : 'dummy-0';
const tagNumber = /\w+-(\d+)/.exec(lastTag)[1] | 0;
const channel = repo.split('-')[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.');
}
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) {
let baseUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/`;
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!) {
repository(name:$name, owner:$owner) {
pullRequests(labels: [$label], states: OPEN, first: 100) {
@ -250,49 +215,13 @@ async function getPulls(github) {
}
}
}`;
const mainlineVariables = {
const variables = {
owner: 'yuzu-emu',
name: 'yuzu',
label: CHANGE_LABEL_MAINLINE,
label: CHANGE_LABEL,
};
const mainlineResult = await github.graphql(query, mainlineVariables);
const pulls = mainlineResult.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);
const result = await github.graphql(query, variables);
const pulls = result.repository.pullRequests.nodes;
let displayList = [];
for (let i = 0; i < pulls.length; i++) {
let pull = pulls[i];
@ -302,17 +231,11 @@ async function mergebot(github, context, execa) {
console.table(displayList);
await fetchPullRequests(pulls, "https://github.com/yuzu-emu/yuzu", execa);
const mergeResults = await mergePullRequests(pulls, execa);
if (BUILD_EA) {
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.checkAndroidChanges = checkAndroidChanges;
module.exports.tagAndPush = tagAndPush;
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
name: yuzu-android-publish
@ -16,7 +16,7 @@ on:
jobs:
android:
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:
# this checkout is required to make sure the GitHub Actions scripts are available
- uses: actions/checkout@v3

View File

@ -73,7 +73,7 @@ jobs:
build-mac:
name: 'test build (macos)'
needs: format
runs-on: macos-14
runs-on: macos-13
steps:
- uses: actions/checkout@v3
with:
@ -81,12 +81,13 @@ jobs:
fetch-depth: 0
- name: Install dependencies
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
run: |
mkdir build
cd build
export Qt5_DIR="$(brew --prefix qt@5)/lib/cmake"
export Qt5_DIR="/usr/local/opt/qt@5/lib/cmake"
cmake .. -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DYUZU_USE_BUNDLED_VCPKG=OFF -DYUZU_TESTS=OFF -DENABLE_WEB_SERVICE=OFF -DENABLE_LIBUSB=OFF
ninja
build-msvc:

3
.gitmodules vendored
View File

@ -64,6 +64,3 @@
[submodule "oaknut"]
path = externals/oaknut
url = https://github.com/merryhime/oaknut
[submodule "Vulkan-Utility-Libraries"]
path = externals/Vulkan-Utility-Libraries
url = https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git

View File

@ -36,8 +36,6 @@ option(YUZU_USE_BUNDLED_FFMPEG "Download/Build bundled FFmpeg" "${WIN32}")
option(YUZU_USE_EXTERNAL_VULKAN_HEADERS "Use Vulkan-Headers from externals" ON)
option(YUZU_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES "Use Vulkan-Utility-Libraries from externals" ON)
option(YUZU_USE_QT_MULTIMEDIA "Use QtMultimedia for Camera" OFF)
option(YUZU_USE_QT_WEB_ENGINE "Use QtWebEngine for web applet implementation" OFF)
@ -307,11 +305,7 @@ find_package(ZLIB 1.2 REQUIRED)
find_package(zstd 1.5 REQUIRED)
if (NOT YUZU_USE_EXTERNAL_VULKAN_HEADERS)
find_package(VulkanHeaders 1.3.274 REQUIRED)
endif()
if (NOT YUZU_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES)
find_package(VulkanUtilityLibraries REQUIRED)
find_package(Vulkan 1.3.274 REQUIRED)
endif()
if (ENABLE_LIBUSB)
@ -322,10 +316,6 @@ if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
find_package(xbyak 6 CONFIG)
endif()
if (ARCHITECTURE_arm64)
find_package(oaknut 2.0.1 CONFIG)
endif()
if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64)
find_package(dynarmic 6.4.0 CONFIG)
endif()

View File

@ -1,9 +1,9 @@
| 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 |
| [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 |
| [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 |
| [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 |
| [12749](https://github.com/yuzu-emu/yuzu-android//pull/12749) | [`e3171486d`](https://github.com/yuzu-emu/yuzu-android//pull/12749/files) | general: workarounds for SMMU syncing issues | [liamwhite](https://github.com/liamwhite/) | Yes |
| [12760](https://github.com/yuzu-emu/yuzu-android//pull/12760) | [`c6b88b62d`](https://github.com/yuzu-emu/yuzu-android//pull/12760/files) | am: rewrite for multiprocess support | [liamwhite](https://github.com/liamwhite/) | Yes |
| [12814](https://github.com/yuzu-emu/yuzu-android//pull/12814) | [`da410506a`](https://github.com/yuzu-emu/yuzu-android//pull/12814/files) | Move time services to new IPC and add debug printing | [Kelebek1](https://github.com/Kelebek1/) | Yes |
| [12830](https://github.com/yuzu-emu/yuzu-android//pull/12830) | [`6cc82fd43`](https://github.com/yuzu-emu/yuzu-android//pull/12830/files) | externals/dynarmic: Update to 6.6.1 | [merryhime](https://github.com/merryhime/) | Yes |
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
source_file = ../../src/android/app/src/main/res/values/strings.xml
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

1761
dist/languages/ar.ts vendored

File diff suppressed because it is too large Load Diff

1809
dist/languages/ca.ts vendored

File diff suppressed because it is too large Load Diff

1771
dist/languages/cs.ts vendored

File diff suppressed because it is too large Load Diff

1763
dist/languages/da.ts vendored

File diff suppressed because it is too large Load Diff

1778
dist/languages/de.ts vendored

File diff suppressed because it is too large Load Diff

1777
dist/languages/el.ts vendored

File diff suppressed because it is too large Load Diff

1871
dist/languages/es.ts vendored

File diff suppressed because it is too large Load Diff

1831
dist/languages/fr.ts vendored

File diff suppressed because it is too large Load Diff

1925
dist/languages/hu.ts vendored

File diff suppressed because it is too large Load Diff

2232
dist/languages/id.ts vendored

File diff suppressed because it is too large Load Diff

1789
dist/languages/it.ts vendored

File diff suppressed because it is too large Load Diff

1795
dist/languages/ja_JP.ts vendored

File diff suppressed because it is too large Load Diff

1777
dist/languages/ko_KR.ts vendored

File diff suppressed because it is too large Load Diff

1777
dist/languages/nb.ts vendored

File diff suppressed because it is too large Load Diff

1777
dist/languages/nl.ts vendored

File diff suppressed because it is too large Load Diff

1775
dist/languages/pl.ts vendored

File diff suppressed because it is too large Load Diff

2181
dist/languages/pt_BR.ts vendored

File diff suppressed because it is too large Load Diff

1827
dist/languages/pt_PT.ts vendored

File diff suppressed because it is too large Load Diff

1972
dist/languages/ru_RU.ts vendored

File diff suppressed because it is too large Load Diff

1771
dist/languages/sv.ts vendored

File diff suppressed because it is too large Load Diff

1845
dist/languages/tr_TR.ts vendored

File diff suppressed because it is too large Load Diff

1777
dist/languages/uk.ts vendored

File diff suppressed because it is too large Load Diff

1777
dist/languages/vi.ts vendored

File diff suppressed because it is too large Load Diff

1777
dist/languages/vi_VN.ts vendored

File diff suppressed because it is too large Load Diff

1863
dist/languages/zh_CN.ts vendored

File diff suppressed because it is too large Load Diff

1828
dist/languages/zh_TW.ts vendored

File diff suppressed because it is too large Load Diff

View File

@ -14,17 +14,16 @@ set(BUILD_SHARED_LIBS OFF)
# Skip install rules for all externals
set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL ON)
# Xbyak (also used by Dynarmic, so needs to be added first)
# xbyak
if ((ARCHITECTURE_x86 OR ARCHITECTURE_x86_64) AND NOT TARGET xbyak::xbyak)
add_subdirectory(xbyak)
endif()
# Oaknut (also used by Dynarmic, so needs to be added first)
# Dynarmic
if (ARCHITECTURE_arm64 AND NOT TARGET merry::oaknut)
add_subdirectory(oaknut)
endif()
# Dynarmic
if ((ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64) AND NOT TARGET dynarmic::dynarmic)
set(DYNARMIC_IGNORE_ASSERTS ON)
add_subdirectory(dynarmic)
@ -155,11 +154,6 @@ if (YUZU_USE_EXTERNAL_VULKAN_HEADERS)
add_subdirectory(Vulkan-Headers)
endif()
# Vulkan-Utility-Libraries
if (YUZU_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES)
add_subdirectory(Vulkan-Utility-Libraries)
endif()
# TZDB (Time Zone Database)
add_subdirectory(nx_tzdb)
@ -314,10 +308,3 @@ endif()
if (NOT TARGET SimpleIni::SimpleIni)
add_subdirectory(simpleini)
endif()
# sse2neon
if (ARCHITECTURE_arm64 AND NOT TARGET sse2neon)
add_library(sse2neon INTERFACE)
target_include_directories(sse2neon INTERFACE sse2neon)
endif()

@ -1 +0,0 @@
Subproject commit 524f8910d0e4a5f2ec5961996b23e5b74b95cb1d

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-invalid-offsetof
-Wno-unused-parameter
-Wno-missing-field-initializers
)
if (CMAKE_CXX_COMPILER_ID MATCHES Clang) # Clang or AppleClang
@ -165,7 +164,6 @@ else()
if (MINGW)
add_definitions(-DMINGW_HAS_SECURE_API)
add_compile_options("-msse4.1")
if (MINGW_STATIC_BUILD)
add_definitions(-DQT_STATICPLUGIN)

View File

@ -3,8 +3,8 @@
import android.annotation.SuppressLint
import kotlin.collections.setOf
import org.jetbrains.kotlin.konan.properties.Properties
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
import com.github.triplet.gradle.androidpublisher.ReleaseStatus
plugins {
id("com.android.application")
@ -13,7 +13,6 @@ plugins {
kotlin("plugin.serialization") version "1.9.20"
id("androidx.navigation.safeargs.kotlin")
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
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
} else {
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 {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
@ -259,18 +257,12 @@ fun runGitCommand(command: List<String>): String {
}
fun getGitVersion(): String {
val gitVersion = runGitCommand(
listOf(
"git",
"describe",
"--always",
"--long"
)
).replace(Regex("(-0)?-[^-]+$"), "")
val versionName = if (System.getenv("GITHUB_ACTIONS") != null) {
System.getenv("GIT_TAG_NAME") ?: gitVersion
val gitTag = System.getenv("GIT_TAG_NAME") ?: ""
gitTag
} else {
gitVersion
runGitCommand(listOf("git", "describe", "--always", "--long"))
.replace(Regex("(-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-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.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
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" />
</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
android:name=".features.DocumentProvider"
android:authorities="${applicationId}.user"

View File

@ -3,21 +3,24 @@
package org.yuzu.yuzu_emu
import android.app.Dialog
import android.content.DialogInterface
import android.net.Uri
import android.os.Bundle
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.Surface
import android.view.View
import android.widget.TextView
import androidx.annotation.Keep
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.ref.WeakReference
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.FileUtil
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.Patch
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.
*/
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
var sEmulationActivity = WeakReference<EmulationActivity?>(null)
@ -96,6 +127,112 @@ object NativeLibrary {
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)
/**
@ -124,7 +261,7 @@ object NativeLibrary {
/**
* Begins emulation.
*/
external fun run(path: String?, programIndex: Int, frontendInitiated: Boolean)
external fun run(path: String?, programIndex: Int = 0)
// Surface Handling
external fun surfaceChanged(surf: Surface?)
@ -181,13 +318,46 @@ object NativeLibrary {
ErrorUnknown
}
var coreErrorAlertResult = false
val coreErrorAlertLock = Object()
private var coreErrorAlertResult = false
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) {
val emulationActivity = sEmulationActivity.get()
if (emulationActivity == null) {
Log.error("[NativeLibrary] EmulationActivity not present")
error("[NativeLibrary] EmulationActivity not present")
return
}
@ -203,7 +373,7 @@ object NativeLibrary {
fun onCoreError(error: CoreError?, details: String): Boolean {
val emulationActivity = sEmulationActivity.get()
if (emulationActivity == null) {
Log.error("[NativeLibrary] EmulationActivity not present")
error("[NativeLibrary] EmulationActivity not present")
return false
}
@ -234,7 +404,7 @@ object NativeLibrary {
}
// 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.
synchronized(coreErrorAlertLock) { coreErrorAlertLock.wait() }
@ -459,4 +629,46 @@ object NativeLibrary {
* Checks if all necessary keys are present for decryption
*/
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.NotificationManager
import android.content.Context
import org.yuzu.yuzu_emu.features.input.NativeInput
import java.io.File
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.DocumentsTree
@ -18,6 +17,17 @@ fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir
class YuzuApplication : Application() {
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(
getString(R.string.notice_notification_channel_id),
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
// or other notification behaviors after this
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(emulationChannel)
notificationManager.createNotificationChannel(noticeChannel)
}
@ -38,7 +49,6 @@ class YuzuApplication : Application() {
documentsTree = DocumentsTree()
DirectoryInitialization.start()
GpuDriverHelper.initializeDriverParameters()
NativeInput.reloadInputDevices()
NativeLibrary.logDeviceInfo()
Log.logDeviceInfo()

View File

@ -4,6 +4,7 @@
package org.yuzu.yuzu_emu.activities
import android.annotation.SuppressLint
import android.app.Activity
import android.app.PendingIntent
import android.app.PictureInPictureParams
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.YuzuApplication
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.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.model.EmulationViewModel
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.Log
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.ParamPackage
import org.yuzu.yuzu_emu.utils.ThemeHelper
import java.text.NumberFormat
import kotlin.math.roundToInt
@ -66,6 +65,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private var motionTimestamp: Long = 0
private var flipMotionOrientation: Boolean = false
private var controllerIds = InputHandler.getGameControllerIds()
private val actionPause = "ACTION_EMULATOR_PAUSE"
private val actionPlay = "ACTION_EMULATOR_PLAY"
private val actionMute = "ACTION_EMULATOR_MUTE"
@ -73,39 +74,17 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private val emulationViewModel: EmulationViewModel by viewModels()
override fun onDestroy() {
stopForegroundService(this)
super.onDestroy()
}
override fun onCreate(savedInstanceState: Bundle?) {
Log.gameLaunched = true
ThemeHelper.setTheme(this)
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)
setContentView(binding.root)
@ -123,6 +102,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
nfcReader = NfcReader(this)
nfcReader.initialize()
InputHandler.initialize()
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) {
if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) {
@ -144,6 +125,10 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
.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 {
@ -173,7 +158,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
super.onResume()
nfcReader.startScanning()
startMotionSensorListener()
InputHandler.updateControllerData()
InputHandler.updateControllerIds()
buildPictureInPictureParams()
}
@ -198,7 +183,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
super.onNewIntent(intent)
setIntent(intent)
nfcReader.onNewIntent(intent)
InputHandler.updateControllerData()
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@ -271,8 +255,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
}
val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000
motionTimestamp = event.timestamp
NativeInput.onDeviceMotionEvent(
NativeInput.Player1Device,
NativeLibrary.onGamePadMotionEvent(
NativeLibrary.Player1Device,
deltaTimestamp,
gyro[0],
gyro[1],
@ -281,8 +265,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
accel[1],
accel[2]
)
NativeInput.onDeviceMotionEvent(
NativeInput.ConsoleDevice,
NativeLibrary.onGamePadMotionEvent(
NativeLibrary.ConsoleDevice,
deltaTimestamp,
gyro[0],
gyro[1],
@ -497,6 +481,12 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
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 {
if (view == null) {
return true

View File

@ -3,15 +3,14 @@
package org.yuzu.yuzu_emu.adapters
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.model.Driver
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
class DriverAdapter(private val driverViewModel: DriverViewModel) :
@ -44,15 +43,29 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :
}
// Delay marquee by 3s
title.marquee()
version.marquee()
description.marquee()
title.postDelayed(
{
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
version.text = model.version
description.text = model.description
buttonDelete.setVisible(
model.title != binding.root.context.getString(R.string.system_gpu_driver)
)
if (model.description.isNotEmpty()) {
version.visibility = View.VISIBLE
description.visibility = View.VISIBLE
buttonDelete.visibility = View.VISIBLE
} else {
version.visibility = View.GONE
description.visibility = View.GONE
buttonDelete.visibility = View.GONE
}
}
}
}

View File

@ -4,6 +4,7 @@
package org.yuzu.yuzu_emu.adapters
import android.net.Uri
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.ViewGroup
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.model.GameDir
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
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) {
binding.apply {
path.text = Uri.parse(model.uriString).path
path.marquee()
path.postDelayed(
{
path.isSelected = true
path.ellipsize = TextUtils.TruncateAt.MARQUEE
},
3000
)
buttonEdit.setOnClickListener {
GameFolderPropertiesDialogFragment.newInstance(model)

View File

@ -4,6 +4,7 @@
package org.yuzu.yuzu_emu.adapters
import android.net.Uri
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.ViewGroup
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.GamesViewModel
import org.yuzu.yuzu_emu.utils.GameIconUtils
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
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.marquee()
binding.textGameTitle.postDelayed(
{
binding.textGameTitle.ellipsize = TextUtils.TruncateAt.MARQUEE
binding.textGameTitle.isSelected = true
},
3000
)
binding.cardGame.setOnClickListener { onClick(model) }
binding.cardGame.setOnLongClickListener { onLongClick(model) }
}

View File

@ -3,18 +3,21 @@
package org.yuzu.yuzu_emu.adapters
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.Lifecycle
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.CardSimpleOutlinedBinding
import org.yuzu.yuzu_emu.model.GameProperty
import org.yuzu.yuzu_emu.model.InstallableProperty
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
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) {
binding.details.setVisible(true)
binding.details.visibility = View.VISIBLE
binding.details.text = submenuProperty.details.invoke()
} else if (submenuProperty.detailsFlow != null) {
binding.details.setVisible(true)
submenuProperty.detailsFlow.collect(viewLifecycle) { binding.details.text = it }
binding.details.visibility = View.VISIBLE
viewLifecycle.lifecycleScope.launch {
viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
submenuProperty.detailsFlow.collect { binding.details.text = it }
}
}
} else {
binding.details.setVisible(false)
binding.details.visibility = View.GONE
}
}
}
@ -101,10 +112,14 @@ class GamePropertiesAdapter(
)
)
binding.buttonInstall.setVisible(installableProperty.install != null)
binding.buttonInstall.setOnClickListener { installableProperty.install?.invoke() }
binding.buttonExport.setVisible(installableProperty.export != null)
binding.buttonExport.setOnClickListener { installableProperty.export?.invoke() }
if (installableProperty.install != null) {
binding.buttonInstall.visibility = View.VISIBLE
binding.buttonInstall.setOnClickListener { installableProperty.install.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
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.Lifecycle
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.databinding.CardHomeOptionBinding
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
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
class HomeSettingAdapter(
@ -56,8 +59,18 @@ class HomeSettingAdapter(
binding.optionIcon.alpha = 0.5f
}
model.details.collect(viewLifecycle) { updateOptionDetails(it) }
binding.optionDetail.marquee()
viewLifecycle.lifecycleScope.launch {
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) }
}
@ -77,7 +90,7 @@ class HomeSettingAdapter(
private fun updateOptionDetails(detailString: String) {
if (detailString.isNotEmpty()) {
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
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.yuzu.yuzu_emu.databinding.CardInstallableBinding
import org.yuzu.yuzu_emu.model.Installable
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class InstallableAdapter(installables: List<Installable>) :
@ -26,10 +26,14 @@ class InstallableAdapter(installables: List<Installable>) :
binding.title.setText(model.titleId)
binding.description.setText(model.descriptionId)
binding.buttonInstall.setVisible(model.install != null)
binding.buttonInstall.setOnClickListener { model.install?.invoke() }
binding.buttonExport.setVisible(model.export != null)
binding.buttonExport.setOnClickListener { model.export?.invoke() }
if (model.install != null) {
binding.buttonInstall.visibility = View.VISIBLE
binding.buttonInstall.setOnClickListener { model.install.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
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment
import org.yuzu.yuzu_emu.model.License
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) :
@ -25,7 +25,7 @@ class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<Lic
binding.apply {
textSettingName.text = root.context.getString(model.titleId)
textSettingDescription.text = root.context.getString(model.descriptionId)
textSettingValue.setVisible(false)
textSettingValue.visibility = View.GONE
root.setOnClickListener { onClick(model) }
}

View File

@ -5,6 +5,7 @@ package org.yuzu.yuzu_emu.adapters
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
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.StepState
import org.yuzu.yuzu_emu.utils.ViewUtils
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
@ -30,8 +30,8 @@ class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
AbstractViewHolder<SetupPage>(binding), SetupCallback {
override fun bind(model: SetupPage) {
if (model.stepCompleted.invoke() == StepState.COMPLETE) {
binding.buttonAction.setVisible(visible = false, gone = false)
binding.textConfirmation.setVisible(true)
binding.buttonAction.visibility = View.INVISIBLE
binding.textConfirmation.visibility = View.VISIBLE
}
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"),
SHOW_PERFORMANCE_OVERLAY("show_performance_overlay"),
SHOW_INPUT_OVERLAY("show_input_overlay"),
TOUCHSCREEN("touchscreen"),
SHOW_THERMAL_OVERLAY("show_thermal_overlay");
TOUCHSCREEN("touchscreen");
override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeConfig.getBoolean(key, needsGlobal)

View File

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

View File

@ -4,30 +4,17 @@
package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
object Settings {
enum class MenuTag(val titleId: Int = 0) {
enum class MenuTag(val titleId: Int) {
SECTION_ROOT(R.string.advanced_settings),
SECTION_SYSTEM(R.string.preferences_system),
SECTION_RENDERER(R.string.preferences_graphics),
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_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_MEMORY_WARNING_SHOWN = "MemoryWarningShown"
@ -106,15 +93,4 @@ object Settings {
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
enum class StringSetting(override val key: String) : AbstractStringSetting {
DRIVER_PATH("driver_path"),
DEVICE_NAME("device_name");
DRIVER_PATH("driver_path");
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
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting
class DateTimeSetting(
private val longSetting: AbstractLongSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = ""
) : SettingsItem(longSetting, titleId, titleString, descriptionId, descriptionString) {
titleId: Int,
descriptionId: Int
) : SettingsItem(longSetting, titleId, descriptionId) {
override val type = TYPE_DATETIME_SETTING
fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal)

View File

@ -3,11 +3,8 @@
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
class HeaderSetting(
@StringRes titleId: Int = 0,
titleString: String = ""
) : SettingsItem(emptySetting, titleId, titleString, 0, "") {
titleId: Int
) : SettingsItem(emptySetting, titleId, 0) {
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
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
class RunnableSetting(
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
val isRunnable: Boolean,
titleId: Int,
descriptionId: Int,
val isRuntimeRunnable: Boolean,
@DrawableRes val iconId: Int = 0,
val runnable: () -> Unit
) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) {
) : SettingsItem(emptySetting, titleId, descriptionId) {
override val type = TYPE_RUNNABLE
}

View File

@ -3,12 +3,8 @@
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.NativeLibrary
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.AbstractSetting
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.LongSetting
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
/**
@ -28,34 +23,13 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
*/
abstract class SettingsItem(
val setting: AbstractSetting,
@StringRes val titleId: Int,
val titleString: String,
@StringRes val descriptionId: Int,
val descriptionString: String
val nameId: Int,
val descriptionId: 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
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
if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
return false
@ -76,9 +50,6 @@ abstract class SettingsItem(
get() = NativeLibrary.isRunning() && !setting.global &&
!NativeConfig.isPerGameConfigLoaded()
val clearable: Boolean
get() = !setting.global && NativeConfig.isPerGameConfigLoaded()
companion object {
const val TYPE_HEADER = 0
const val TYPE_SWITCH = 1
@ -88,10 +59,6 @@ abstract class SettingsItem(
const val TYPE_STRING_SINGLE_CHOICE = 5
const val TYPE_DATETIME_SETTING = 6
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"
@ -110,246 +77,221 @@ abstract class SettingsItem(
// List of all general
val settingsItems = HashMap<String, SettingsItem>().apply {
put(StringInputSetting(StringSetting.DEVICE_NAME, titleId = R.string.device_name))
put(
SwitchSetting(
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
titleId = R.string.frame_limit_enable,
descriptionId = R.string.frame_limit_enable_description
R.string.frame_limit_enable,
R.string.frame_limit_enable_description
)
)
put(
SliderSetting(
ShortSetting.RENDERER_SPEED_LIMIT,
titleId = R.string.frame_limit_slider,
descriptionId = R.string.frame_limit_slider_description,
min = 1,
max = 400,
units = "%"
R.string.frame_limit_slider,
R.string.frame_limit_slider_description,
1,
400,
"%"
)
)
put(
SingleChoiceSetting(
IntSetting.CPU_BACKEND,
titleId = R.string.cpu_backend,
choicesId = R.array.cpuBackendArm64Names,
valuesId = R.array.cpuBackendArm64Values
R.string.cpu_backend,
0,
R.array.cpuBackendArm64Names,
R.array.cpuBackendArm64Values
)
)
put(
SingleChoiceSetting(
IntSetting.CPU_ACCURACY,
titleId = R.string.cpu_accuracy,
choicesId = R.array.cpuAccuracyNames,
valuesId = R.array.cpuAccuracyValues
R.string.cpu_accuracy,
0,
R.array.cpuAccuracyNames,
R.array.cpuAccuracyValues
)
)
put(
SwitchSetting(
BooleanSetting.PICTURE_IN_PICTURE,
titleId = R.string.picture_in_picture,
descriptionId = R.string.picture_in_picture_description
R.string.picture_in_picture,
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(
SwitchSetting(
dockedModeSetting,
titleId = R.string.use_docked_mode,
descriptionId = R.string.use_docked_mode_description
BooleanSetting.USE_DOCKED_MODE,
R.string.use_docked_mode,
R.string.use_docked_mode_description
)
)
put(
SingleChoiceSetting(
IntSetting.REGION_INDEX,
titleId = R.string.emulated_region,
choicesId = R.array.regionNames,
valuesId = R.array.regionValues
R.string.emulated_region,
0,
R.array.regionNames,
R.array.regionValues
)
)
put(
SingleChoiceSetting(
IntSetting.LANGUAGE_INDEX,
titleId = R.string.emulated_language,
choicesId = R.array.languageNames,
valuesId = R.array.languageValues
R.string.emulated_language,
0,
R.array.languageNames,
R.array.languageValues
)
)
put(
SwitchSetting(
BooleanSetting.USE_CUSTOM_RTC,
titleId = R.string.use_custom_rtc,
descriptionId = R.string.use_custom_rtc_description
R.string.use_custom_rtc,
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(
SingleChoiceSetting(
IntSetting.RENDERER_ACCURACY,
titleId = R.string.renderer_accuracy,
choicesId = R.array.rendererAccuracyNames,
valuesId = R.array.rendererAccuracyValues
R.string.renderer_accuracy,
0,
R.array.rendererAccuracyNames,
R.array.rendererAccuracyValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_RESOLUTION,
titleId = R.string.renderer_resolution,
choicesId = R.array.rendererResolutionNames,
valuesId = R.array.rendererResolutionValues
R.string.renderer_resolution,
0,
R.array.rendererResolutionNames,
R.array.rendererResolutionValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_VSYNC,
titleId = R.string.renderer_vsync,
choicesId = R.array.rendererVSyncNames,
valuesId = R.array.rendererVSyncValues
R.string.renderer_vsync,
0,
R.array.rendererVSyncNames,
R.array.rendererVSyncValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_SCALING_FILTER,
titleId = R.string.renderer_scaling_filter,
choicesId = R.array.rendererScalingFilterNames,
valuesId = R.array.rendererScalingFilterValues
)
)
put(
SliderSetting(
IntSetting.FSR_SHARPENING_SLIDER,
titleId = R.string.fsr_sharpness,
descriptionId = R.string.fsr_sharpness_description,
units = "%"
R.string.renderer_scaling_filter,
0,
R.array.rendererScalingFilterNames,
R.array.rendererScalingFilterValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_ANTI_ALIASING,
titleId = R.string.renderer_anti_aliasing,
choicesId = R.array.rendererAntiAliasingNames,
valuesId = R.array.rendererAntiAliasingValues
R.string.renderer_anti_aliasing,
0,
R.array.rendererAntiAliasingNames,
R.array.rendererAntiAliasingValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_SCREEN_LAYOUT,
titleId = R.string.renderer_screen_layout,
choicesId = R.array.rendererScreenLayoutNames,
valuesId = R.array.rendererScreenLayoutValues
R.string.renderer_screen_layout,
0,
R.array.rendererScreenLayoutNames,
R.array.rendererScreenLayoutValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_ASPECT_RATIO,
titleId = R.string.renderer_aspect_ratio,
choicesId = R.array.rendererAspectRatioNames,
valuesId = R.array.rendererAspectRatioValues
)
)
put(
SingleChoiceSetting(
IntSetting.VERTICAL_ALIGNMENT,
titleId = R.string.vertical_alignment,
descriptionId = 0,
choicesId = R.array.verticalAlignmentEntries,
valuesId = R.array.verticalAlignmentValues
R.string.renderer_aspect_ratio,
0,
R.array.rendererAspectRatioNames,
R.array.rendererAspectRatioValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE,
titleId = R.string.use_disk_shader_cache,
descriptionId = R.string.use_disk_shader_cache_description
R.string.use_disk_shader_cache,
R.string.use_disk_shader_cache_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_FORCE_MAX_CLOCK,
titleId = R.string.renderer_force_max_clock,
descriptionId = R.string.renderer_force_max_clock_description
R.string.renderer_force_max_clock,
R.string.renderer_force_max_clock_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS,
titleId = R.string.renderer_asynchronous_shaders,
descriptionId = R.string.renderer_asynchronous_shaders_description
R.string.renderer_asynchronous_shaders,
R.string.renderer_asynchronous_shaders_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_REACTIVE_FLUSHING,
titleId = R.string.renderer_reactive_flushing,
descriptionId = R.string.renderer_reactive_flushing_description
R.string.renderer_reactive_flushing,
R.string.renderer_reactive_flushing_description
)
)
put(
SingleChoiceSetting(
IntSetting.MAX_ANISOTROPY,
titleId = R.string.anisotropic_filtering,
descriptionId = R.string.anisotropic_filtering_description,
choicesId = R.array.anisoEntries,
valuesId = R.array.anisoValues
R.string.anisotropic_filtering,
R.string.anisotropic_filtering_description,
R.array.anisoEntries,
R.array.anisoValues
)
)
put(
SingleChoiceSetting(
IntSetting.AUDIO_OUTPUT_ENGINE,
titleId = R.string.audio_output_engine,
choicesId = R.array.outputEngineEntries,
valuesId = R.array.outputEngineValues
R.string.audio_output_engine,
0,
R.array.outputEngineEntries,
R.array.outputEngineValues
)
)
put(
SliderSetting(
ByteSetting.AUDIO_VOLUME,
titleId = R.string.audio_volume,
descriptionId = R.string.audio_volume_description,
units = "%"
R.string.audio_volume,
R.string.audio_volume_description,
0,
100,
"%"
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_BACKEND,
titleId = R.string.renderer_api,
choicesId = R.array.rendererApiNames,
valuesId = R.array.rendererApiValues
R.string.renderer_api,
0,
R.array.rendererApiNames,
R.array.rendererApiValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_DEBUG,
titleId = R.string.renderer_debug,
descriptionId = R.string.renderer_debug_description
R.string.renderer_debug,
R.string.renderer_debug_description
)
)
put(
SwitchSetting(
BooleanSetting.CPU_DEBUG_MODE,
titleId = R.string.cpu_debug_mode,
descriptionId = R.string.cpu_debug_mode_description
R.string.cpu_debug_mode,
R.string.cpu_debug_mode_description
)
)
@ -385,7 +327,7 @@ abstract class SettingsItem(
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
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.AbstractSetting
class SingleChoiceSetting(
setting: AbstractSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
@ArrayRes val choicesId: Int,
@ArrayRes val valuesId: Int
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
titleId: Int,
descriptionId: Int,
val choicesId: Int,
val valuesId: Int
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SINGLE_CHOICE
fun getSelectedValue(needsGlobal: Boolean = false) =

View File

@ -3,7 +3,6 @@
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.AbstractFloatSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
@ -13,14 +12,12 @@ import kotlin.math.roundToInt
class SliderSetting(
setting: AbstractSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
val min: Int = 0,
val max: Int = 100,
val units: String = ""
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
titleId: Int,
descriptionId: Int,
val min: Int,
val max: Int,
val units: String
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SLIDER
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
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
class StringSingleChoiceSetting(
private val stringSetting: AbstractStringSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
titleId: Int,
descriptionId: Int,
val choices: Array<String>,
val values: Array<String>
) : SettingsItem(stringSetting, titleId, titleString, descriptionId, descriptionString) {
) : SettingsItem(stringSetting, titleId, descriptionId) {
override val type = TYPE_STRING_SINGLE_CHOICE
fun getValueAt(index: Int): String =
@ -23,7 +20,7 @@ class StringSingleChoiceSetting(
fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)
fun setSelectedValue(value: String) = stringSetting.setString(value)
val selectedValueIndex: Int
val selectValueIndex: Int
get() {
for (i in values.indices) {
if (values[i] == getSelectedValue()) {

View File

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

View File

@ -3,18 +3,15 @@
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.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
class SwitchSetting(
setting: AbstractSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = ""
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
titleId: Int,
descriptionId: Int
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SWITCH
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.WindowCompat
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.navArgs
import com.google.android.material.color.MaterialColors
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.NativeLibrary
import java.io.IOException
import org.yuzu.yuzu_emu.R
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.fragments.ResetSettingsDialogFragment
import org.yuzu.yuzu_emu.model.SettingsViewModel
import org.yuzu.yuzu_emu.utils.*
class SettingsActivity : AppCompatActivity() {
@ -65,25 +70,41 @@ class SettingsActivity : AppCompatActivity() {
)
}
settingsViewModel.shouldRecreate.collect(
this,
resetState = { settingsViewModel.setShouldRecreate(false) }
) { if (it) recreate() }
settingsViewModel.shouldNavigateBack.collect(
this,
resetState = { settingsViewModel.setShouldNavigateBack(false) }
) { if (it) navigateBack() }
settingsViewModel.shouldShowResetSettingsDialog.collect(
this,
resetState = { settingsViewModel.setShouldShowResetSettingsDialog(false) }
) {
lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.shouldRecreate.collectLatest {
if (it) {
settingsViewModel.setShouldRecreate(false)
recreate()
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.shouldNavigateBack.collectLatest {
if (it) {
settingsViewModel.setShouldNavigateBack(false)
navigateBack()
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.shouldShowResetSettingsDialog.collectLatest {
if (it) {
settingsViewModel.setShouldShowResetSettingsDialog(false)
ResetSettingsDialogFragment().show(
supportFragmentManager,
ResetSettingsDialogFragment.TAG
)
}
}
}
}
}
onBackPressedDispatcher.addCallback(
this,
@ -116,7 +137,6 @@ class SettingsActivity : AppCompatActivity() {
super.onStop()
Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
if (isFinishing) {
NativeInput.reloadInputDevices()
NativeLibrary.applySettings()
if (args.game == null) {
NativeConfig.saveGlobalConfig()

View File

@ -8,11 +8,12 @@ import android.icu.util.Calendar
import android.icu.util.TimeZone
import android.text.format.DateFormat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.recyclerview.widget.AsyncDifferConfig
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.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.SettingsNavigationDirections
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.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.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(
private val fragment: Fragment,
@ -42,6 +41,19 @@ class SettingsAdapter(
private val settingsViewModel: SettingsViewModel
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 {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
@ -73,23 +85,8 @@ class SettingsAdapter(
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 -> {
// TODO: Create an error view since we can't return null now
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
}
}
@ -129,15 +126,6 @@ class SettingsAdapter(
).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) {
val storedTime = item.getValue() * 1000
@ -197,214 +185,6 @@ class SettingsAdapter(
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 {
SettingsDialogFragment.newInstance(
settingsViewModel,

View File

@ -8,22 +8,25 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
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.databinding.FragmentSettingsBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
import org.yuzu.yuzu_emu.model.SettingsViewModel
class SettingsFragment : Fragment() {
private lateinit var presenter: SettingsFragmentPresenter
@ -42,12 +45,6 @@ class SettingsFragment : Fragment() {
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
val playerIndex = getPlayerIndex()
if (playerIndex != -1) {
NativeInput.loadInputProfiles()
NativeInput.reloadInputDevices()
}
}
override fun onCreateView(
@ -59,9 +56,9 @@ class SettingsFragment : Fragment() {
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?) {
super.onViewCreated(view, savedInstanceState)
settingsAdapter = SettingsAdapter(this, requireContext())
presenter = SettingsFragmentPresenter(
settingsViewModel,
@ -74,17 +71,7 @@ class SettingsFragment : Fragment() {
) {
args.game!!.title
} else {
when (args.menuTag) {
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)
}
getString(args.menuTag.titleId)
}
binding.listSettings.apply {
adapter = settingsAdapter
@ -95,37 +82,16 @@ class SettingsFragment : Fragment() {
settingsViewModel.setShouldNavigateBack(true)
}
settingsViewModel.shouldReloadSettingsList.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setShouldReloadSettingsList(false) }
) { if (it) presenter.loadSettingsList() }
settingsViewModel.adapterItemChanged.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setAdapterItemChanged(-1) }
) { 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) }
) {
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.shouldReloadSettingsList.collectLatest {
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)
settingsViewModel.setShouldReloadSettingsList(false)
presenter.loadSettingsList()
}
}
}
}
}
@ -149,19 +115,6 @@ class SettingsFragment : Fragment() {
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() {
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
@ -172,10 +125,18 @@ class SettingsFragment : Fragment() {
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
binding.listSettings.updateMargins(left = leftInsets, right = rightInsets)
binding.listSettings.updatePadding(bottom = barInsets.bottom)
val mlpSettingsList = binding.listSettings.layoutParams as MarginLayoutParams
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
}
}

View File

@ -3,17 +3,11 @@
package org.yuzu.yuzu_emu.features.settings.ui
import android.annotation.SuppressLint
import android.os.Build
import android.widget.Toast
import org.yuzu.yuzu_emu.NativeLibrary
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.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.AbstractIntSetting
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.LongSetting
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.StringSetting
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
class SettingsFragmentPresenter(
private val settingsViewModel: SettingsViewModel,
private val adapter: SettingsAdapter,
private var menuTag: MenuTag
private var menuTag: Settings.MenuTag
) {
private var settingsList = ArrayList<SettingsItem>()
private val context get() = YuzuApplication.appContext
// Extension for altering settings list based on each setting's properties
fun ArrayList<SettingsItem>.add(key: String) {
val item = SettingsItem.settingsItems[key]!!
@ -63,90 +53,73 @@ class SettingsFragmentPresenter(
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() {
loadSettingsList()
}
@SuppressLint("NotifyDataSetChanged")
fun loadSettingsList(notifyDataSetChanged: Boolean = false) {
fun loadSettingsList() {
val sl = ArrayList<SettingsItem>()
when (menuTag) {
MenuTag.SECTION_ROOT -> addConfigSettings(sl)
MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
MenuTag.SECTION_AUDIO -> addAudioSettings(sl)
MenuTag.SECTION_INPUT -> addInputSettings(sl)
MenuTag.SECTION_INPUT_PLAYER_ONE -> addInputPlayer(sl, 0)
MenuTag.SECTION_INPUT_PLAYER_TWO -> addInputPlayer(sl, 1)
MenuTag.SECTION_INPUT_PLAYER_THREE -> addInputPlayer(sl, 2)
MenuTag.SECTION_INPUT_PLAYER_FOUR -> addInputPlayer(sl, 3)
MenuTag.SECTION_INPUT_PLAYER_FIVE -> addInputPlayer(sl, 4)
MenuTag.SECTION_INPUT_PLAYER_SIX -> addInputPlayer(sl, 5)
MenuTag.SECTION_INPUT_PLAYER_SEVEN -> addInputPlayer(sl, 6)
MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7)
MenuTag.SECTION_THEME -> addThemeSettings(sl)
MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
Settings.MenuTag.SECTION_ROOT -> addConfigSettings(sl)
Settings.MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
Settings.MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
Settings.MenuTag.SECTION_AUDIO -> addAudioSettings(sl)
Settings.MenuTag.SECTION_THEME -> addThemeSettings(sl)
Settings.MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
else -> {
val context = YuzuApplication.appContext
Toast.makeText(
context,
context.getString(R.string.unimplemented_menu),
Toast.LENGTH_SHORT
).show()
return
}
}
settingsList = sl
adapter.submitList(settingsList) {
if (notifyDataSetChanged) {
adapter.notifyDataSetChanged()
}
}
adapter.submitList(settingsList)
}
private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
add(
SubmenuSetting(
titleId = R.string.preferences_system,
descriptionId = R.string.preferences_system_description,
iconId = R.drawable.ic_system_settings,
menuKey = MenuTag.SECTION_SYSTEM
R.string.preferences_system,
R.string.preferences_system_description,
R.drawable.ic_system_settings,
Settings.MenuTag.SECTION_SYSTEM
)
)
add(
SubmenuSetting(
titleId = R.string.preferences_graphics,
descriptionId = R.string.preferences_graphics_description,
iconId = R.drawable.ic_graphics,
menuKey = MenuTag.SECTION_RENDERER
R.string.preferences_graphics,
R.string.preferences_graphics_description,
R.drawable.ic_graphics,
Settings.MenuTag.SECTION_RENDERER
)
)
add(
SubmenuSetting(
titleId = R.string.preferences_audio,
descriptionId = R.string.preferences_audio_description,
iconId = R.drawable.ic_audio,
menuKey = MenuTag.SECTION_AUDIO
R.string.preferences_audio,
R.string.preferences_audio_description,
R.drawable.ic_audio,
Settings.MenuTag.SECTION_AUDIO
)
)
add(
SubmenuSetting(
titleId = R.string.preferences_debug,
descriptionId = R.string.preferences_debug_description,
iconId = R.drawable.ic_code,
menuKey = MenuTag.SECTION_DEBUG
R.string.preferences_debug,
R.string.preferences_debug_description,
R.drawable.ic_code,
Settings.MenuTag.SECTION_DEBUG
)
)
add(
RunnableSetting(
titleId = R.string.reset_to_default,
descriptionId = R.string.reset_to_default_description,
isRunnable = !NativeLibrary.isRunning(),
iconId = R.drawable.ic_restore
R.string.reset_to_default,
R.string.reset_to_default_description,
false,
R.drawable.ic_restore
) { settingsViewModel.setShouldShowResetSettingsDialog(true) }
)
}
@ -154,7 +127,6 @@ class SettingsFragmentPresenter(
private fun addSystemSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
add(StringSetting.DEVICE_NAME.key)
add(BooleanSetting.RENDERER_USE_SPEED_LIMIT.key)
add(ShortSetting.RENDERER_SPEED_LIMIT.key)
add(BooleanSetting.USE_DOCKED_MODE.key)
@ -171,12 +143,10 @@ class SettingsFragmentPresenter(
add(IntSetting.RENDERER_RESOLUTION.key)
add(IntSetting.RENDERER_VSYNC.key)
add(IntSetting.RENDERER_SCALING_FILTER.key)
add(IntSetting.FSR_SHARPENING_SLIDER.key)
add(IntSetting.RENDERER_ANTI_ALIASING.key)
add(IntSetting.MAX_ANISOTROPY.key)
add(IntSetting.RENDERER_SCREEN_LAYOUT.key)
add(IntSetting.RENDERER_ASPECT_RATIO.key)
add(IntSetting.VERTICAL_ALIGNMENT.key)
add(BooleanSetting.PICTURE_IN_PICTURE.key)
add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.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>) {
sl.apply {
val theme: AbstractIntSetting = object : AbstractIntSetting {
@ -879,18 +184,20 @@ class SettingsFragmentPresenter(
add(
SingleChoiceSetting(
theme,
titleId = R.string.change_app_theme,
choicesId = R.array.themeEntriesA12,
valuesId = R.array.themeValuesA12
R.string.change_app_theme,
0,
R.array.themeEntriesA12,
R.array.themeValuesA12
)
)
} else {
add(
SingleChoiceSetting(
theme,
titleId = R.string.change_app_theme,
choicesId = R.array.themeEntries,
valuesId = R.array.themeValues
R.string.change_app_theme,
0,
R.array.themeEntries,
R.array.themeValues
)
)
}
@ -919,9 +226,10 @@ class SettingsFragmentPresenter(
add(
SingleChoiceSetting(
themeMode,
titleId = R.string.change_theme_mode,
choicesId = R.array.themeModeEntries,
valuesId = R.array.themeModeValues
R.string.change_theme_mode,
0,
R.array.themeModeEntries,
R.array.themeModeValues
)
)
@ -952,8 +260,8 @@ class SettingsFragmentPresenter(
add(
SwitchSetting(
blackBackgrounds,
titleId = R.string.use_black_backgrounds,
descriptionId = R.string.use_black_backgrounds_description
R.string.use_black_backgrounds,
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.SettingsItem
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) :
SettingViewHolder(binding.root, adapter) {
@ -21,17 +21,28 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
override fun bind(item: SettingsItem) {
setting = item as DateTimeSetting
binding.textSettingName.text = item.title
binding.textSettingDescription.setVisible(item.description.isNotEmpty())
binding.textSettingDescription.text = item.description
binding.textSettingValue.setVisible(true)
binding.textSettingName.setText(item.nameId)
if (item.descriptionId != 0) {
binding.textSettingDescription.setText(item.descriptionId)
binding.textSettingDescription.visibility = View.VISIBLE
} else {
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.visibility = View.VISIBLE
val epochTime = setting.getValue()
val instant = Instant.ofEpochMilli(epochTime * 1000)
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
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 {
adapter.onClearClick(setting, bindingAdapterPosition)
}

View File

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

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