Compare commits

...

185 Commits

Author SHA1 Message Date
f1dca7fd3d Android #57 2023-09-01 00:58:19 +00:00
5ce41fa213 Merge pull request #11420 from t895/long-install-fix
android: Fix game content installer
2023-08-30 23:44:35 -04:00
50d4e0f4f7 android: Fix game content installer
Before this would run on the main thread and freeze the device. Additionally this fixes the result dialog not appearing if a config change happens during the installation by getting the activity's fragment manager when needed.
2023-08-30 19:05:33 -04:00
d833fc383d android: Combine LongMessageDialogFragment with MessageDialogFragment 2023-08-30 18:02:16 -04:00
c5357b17e6 Merge pull request #11416 from t895/shortcuts
android: Support dynamic app shortcuts
2023-08-30 17:43:10 -04:00
b48dbb18f2 android: Support dynamic app shortcuts 2023-08-30 16:26:10 -04:00
a2f0caefd4 Merge pull request #11405 from t895/emulation-loading
android: Emulation loading UI and fixes
2023-08-30 16:24:46 -04:00
5445e974e0 android: Separate emulation states from emulation mutex
Emulation states are repeatedly checked by input and performance stats. During startup and shutdown, this could lead to a long halt on the UI thread because the call to IsRunning will be waiting on the emulation mutex to be unlocked. Using atomics should replace the existing functionality without causing problems.
2023-08-30 15:19:23 -04:00
b0a96d5216 android: Game loading/shutting down indicators 2023-08-30 15:19:23 -04:00
1d07bb4ca6 Merge pull request #11419 from FearlessTobi/hwopus-2
hwopus: Implement GetWorkBufferSizeExEx
2023-08-30 14:16:39 -04:00
a7a5835ffb hwopus: Implement GetWorkBufferSizeExEx
Allows Sea of Stars to boot.

Fixes https://github.com/yuzu-emu/yuzu/issues/11415.
2023-08-30 18:29:43 +02:00
270f430f70 android: Create custom game icon loader 2023-08-30 10:19:05 -04:00
44bce11853 Merge pull request #11380 from t895/settings-integration
android: Settings rework
2023-08-29 22:20:59 -04:00
4701eea646 android: Don't reload settings when stopping settings activity 2023-08-29 21:44:18 -04:00
2f03fac9a0 Merge pull request #11413 from t895/intents
android: Support intents to emulation activity
2023-08-29 20:01:52 -04:00
21ad5f5cc5 android: Add optional androidDefault property to settings
Certain settings have specific defaults for Android only. This lets us reflect them in the Kotlin side with very little code.
2023-08-29 19:42:42 -04:00
45280a0342 android: Proper state restoration on settings dialogs
All dialog code (except for the Date/Time ones) has been extracted out into a generic settings dialog fragment that handles everything through a viewmodel. State for each dialog will now be retained and dialogs will stay shown through configuration changes.

I won't be changing the current state of the date and time dialog fragments until Google decides to make their classes non-final or if/when we migrate to Jetpack Compose.
2023-08-29 19:42:42 -04:00
fd5c7b21dd android: Add search for settings 2023-08-29 19:42:42 -04:00
d786d19880 android: Implement paired settings
Enables and disables editing on settings that rely on other boolean settings.
2023-08-29 19:42:41 -04:00
369d06292f android: Prevent infinite switch toggle loop
If something like a lifecycle event happens when this switch is toggled (Ex. whenever the black backgrounds switch is toggled), this could move the switch from the default position and trigger the checked changed listener and restart the loop. Here I just removed the listener at the start so we recycle the view properly still, set the checked state and then add the new listener.
2023-08-29 19:42:41 -04:00
95a939a49f android: Migrate settings to navigation component
Consolidates all of the settings components to the fragment and activity with no interfaces and only the settings fragment presenter. This also includes new material animations and new viewmodel usage to prevent the fragment and activity directly interacting with one another.
2023-08-29 19:42:41 -04:00
f5e6b12c74 android: Trim settings enums and items
Take advantage of the new settings interface to reduce the amount of code we need for each setting item. Additionally make all settings items non-null to improve brevity.
2023-08-29 19:40:18 -04:00
6c8f2b355a android: Expose interface for getting settings from native code
Completely removes code related to parsing the settings file on the java side. Now all settings are accessed via NativeConfig.kt and config.cpp has been modified to be closer to the core counterpart. Since the core currently uses QSettings, we can't remove reliance from Wini yet. This also includes simplifications to each settings interface to get closer to native code and prepare for per-game settings.
2023-08-29 19:40:17 -04:00
3d5ecc1f08 Merge pull request #11406 from german77/sdl2-28-2
externals: Update SDL to 2.28.2
2023-08-29 09:27:54 -04:00
52c71e7eb6 Merge pull request #11408 from Kelebek1/fix_audio_node_id
[Audio] Fix node id index in DropVoices
2023-08-29 09:27:40 -04:00
1f04a3dd55 Merge pull request #11409 from liamwhite/splatoon-nsd-v2
sfdnsres: ensure lp1 is not resolved
2023-08-29 09:27:32 -04:00
d360abadba Merge pull request #11112 from danilaml/nvdec-deinterlace
Use initial_frame to check interlaced flag
2023-08-29 09:27:22 -04:00
2dbe067d74 android: Support intents to emulation activity 2023-08-29 02:57:20 -04:00
2f18fa5cd1 Merge pull request #11392 from t895/layout-troubles
android: Emulation activity fixes
2023-08-29 02:11:13 -04:00
1615a86375 Merge pull request #11390 from FearlessTobi/hwopus-multi
hwopus: Implement OpenHardwareOpusDecoderForMultiStreamEx and DecodeInterleavedForMultiStream
2023-08-28 19:14:44 -04:00
199de26995 Merge pull request #11399 from liamwhite/cubeb-latency
audio: allow more latency in cubeb initialization
2023-08-28 19:14:33 -04:00
6c68b07a67 sfdnsres: ensure lp1 is not resolved 2023-08-28 11:55:53 -04:00
1d201c71dc Fix node id index in DropVoices 2023-08-28 10:35:30 +01:00
4077ff6851 externals: Update SDL to 2.28.2 2023-08-27 21:08:28 -06:00
164f880f23 Use initial_frame to check interlaced flag
If final frame was transferred from GPU, it won't carry the props.

Fixes #11089
2023-08-28 00:48:53 +04:00
954144e22b audio: allow more latency in cubeb initialization 2023-08-27 12:46:01 -04:00
c2f827b85e hwopus: Implement OpenHardwareOpusDecoderForMultiStreamEx and DecodeInterleavedForMultiStream
Allows MLB The Show 22 to boot.

Fixes https://github.com/yuzu-emu/yuzu/issues/7911.
2023-08-27 18:03:10 +02:00
037f82025c android: Don't set a default emulation orientation
Could cause unnecessary configuration change when setting an orientation other than "Landscape"
2023-08-27 00:19:03 -04:00
338d6f29b1 android: Properly adjust emulation surface aspect ratio
Previously the emulation surface wouldn't respond properly to orientation changes. This would result in the screen appearing stretched when starting in one orientation and switching to another.

The code for calculating the bounds of the view have been changed to match the expected behavior now. Before the view would just match parent in height and width. Now instead of using setLeftTopRightBottom (which is intended to be used for animations) we pass newly calculated bounds for the view into super. Now the view bounds match the emulation output.

This also means that we don't need the overload for the SettingsActivity to launch it using an ActivityResultLauncher. We can just update the view in onResume.
2023-08-27 00:16:53 -04:00
ada4697300 Merge pull request #11389 from FernandoS27/discard-fix
Buffer Cache: fix discard writes.
2023-08-27 04:26:59 +02:00
acc99433c7 Buffer Cache: fix discard writes. 2023-08-27 03:45:43 +02:00
6c4abd23be Merge pull request #11356 from lat9nq/console-mode-pg
general,config-qt: Present Console Mode as an enum with separate options in game properties
2023-08-26 19:15:00 -04:00
84b384fbea Merge pull request #11359 from Kelebek1/check_suitable_backend
Pre-test for valid audio backends
2023-08-26 19:14:47 -04:00
3620533995 Merge pull request #11350 from BenjaminHalko/button-padding
ui: Added padding to the reset button
2023-08-26 19:14:30 -04:00
c5105b65d5 Merge pull request #11317 from Kelebek1/macro_dumps
Mark decompiled macros on dump, dump shaders after translation
2023-08-26 19:14:25 -04:00
1ac2615adb Merge pull request #11338 from comex/warning-fixes-august-2023
Warnings cleanup for GCC 13 and Clang 16
2023-08-26 19:14:17 -04:00
d7a0b8c373 Mark decompiled macros as decompiled on dump, dump shaders after translation 2023-08-25 21:47:47 -04:00
6bb02dcb8a Skip additional mbedcrypto warnings options on MSVC 2023-08-25 19:23:34 -04:00
32c453a5f1 Avoid $<CXX_COMPILER_ID:Clang> because it doesn't include AppleClang. 2023-08-25 19:22:31 -04:00
91eb5afd0b Warnings cleanup for GCC 13 and Clang 16
Note: For GCC there are still a huge number of `-Warray-bounds` warnings
coming from `externals/dynarmic`.  I could have added a workaround in
`externals/CMakeLists.txt` similar to what this PR does for other
externals, but given Dynarmic's close affiliation with Yuzu, it would be
better to fix it upstream.

Besides that, on my machine, this makes the build warning-free except
for some warnings from glslangValidator and AutoMoc.

Details:

- Disable some warnings in externals.

- Disable `-Wnullability-completeness`, which is a Clang warning triggered
  by the Vulkan SDK where if any pointers in the header are marked
  _Nullable, it wants all pointers to be marked _Nullable or _Nonnull.
  Most of them are, but some aren't.  Who knows why.

- `src/web_service/verify_user_jwt.cpp`: Disable another warning when
  including `jwt.hpp`.

- `src/input_common/input_poller.cpp`: Add missing `override` specifiers.

- src/common/swap.h: Remove redundant `operator&`.  In general, this
  file declares three overloads of each operator.  Using `+` as an
  example, the overloads are:

  - a member function for `swapped_t + integer`
  - a member function for `swapped_t + swapped_t`
  - a free function for `integer + swapped_t`

  But for `operator&`, there was an additional free function for
  `swapped_t + integer`, which was redundant with the member function.
  This caused a GCC warning saying "ISO C++ says that these are
  ambiguous".
2023-08-25 19:22:31 -04:00
bc4e58eb51 Merge pull request #11377 from BenjaminHalko/reverse-slider-input
ui: Fixed inverted controls on ReverseSlider widgets
2023-08-25 18:06:03 -04:00
8674724ef0 Merge pull request #11378 from t895/game-flag
android: Use appCategory to specify the app is a game
2023-08-25 18:05:58 -04:00
a8edbb7019 Merge pull request #11370 from FearlessTobi/fix-filesize
filesystem: Return correct error for RenameFile when dest_path already exists
2023-08-25 18:02:54 -04:00
d8c8fbe41f Merge pull request #11371 from FearlessTobi/fix-cli-updates
yuzu/main: Ensure NCAs are registered in content provider when launching from CLI
2023-08-25 18:02:47 -04:00
234cc45192 ssl: tolerate handshake without hostname set (#11328) 2023-08-26 00:02:32 +02:00
b923f5aa7e registered_cache: create fake CNMT entries for program updates of multiprogram applications (#11319) 2023-08-26 00:00:15 +02:00
18ad55be0b kernel: offset code entry point for 39-bit address space type (#11326) 2023-08-25 23:59:32 +02:00
4e71628097 android: Use appCategory to specify the app is a game 2023-08-25 17:17:48 -04:00
92e6ff30a1 Merge pull request #11357 from liamwhite/lime-vfs
android: jni: ensure NCAs from loaded filepath are registered in manual content provider
2023-08-25 13:04:22 -07:00
2e55459e03 Updated to only the reset button 2023-08-25 10:45:42 -07:00
8677d98a10 Updated padded style 2023-08-25 10:29:23 -07:00
49df2b9715 ui: Fixed inverted controls on ReverseSlider widgets
fixes: #11236
2023-08-25 10:06:34 -07:00
2f2de400e1 Merge pull request #11375 from liamwhite/nvhost-as-gpu
nvhost_as_gpu: ensure mappings are aligned to big page size when deallocated
2023-08-25 17:04:16 +02:00
9e134c3da2 nvhost_as_gpu: ensure mappings are aligned to big page size when deallocated 2023-08-25 09:39:18 -04:00
59b3c30f94 yuzu/main: Ensure NCAs are registered in content provider when launching from CLI
Fixes updates and DLC not being loaded when launching yuzu from the command line.

Similar to https://github.com/yuzu-emu/yuzu/pull/11357.
Fixes https://github.com/yuzu-emu/yuzu/issues/8352,
2023-08-24 18:48:02 +02:00
a669e37ddb filesystem: Return correct error for RenameFile when dest_path already exists
Allows Grid Autosport to boot.

Fixes https://github.com/yuzu-emu/yuzu/issues/8287.
2023-08-24 17:07:39 +02:00
7d89f2c146 Merge pull request #11327 from liamwhite/skyline-2
sockets: avoid locking around socket session calls
2023-08-24 10:33:53 -04:00
51ffc2c66c Merge pull request #11367 from FearlessTobi/fix-filesize
game_list_worker: Display correct size for NAX games
2023-08-24 10:33:42 -04:00
e41655960e game_list_worker: Display correct size for NAX games
This was a regression from https://github.com/yuzu-emu/yuzu/pull/1837.

Fixes https://github.com/yuzu-emu/yuzu/issues/1938.
2023-08-24 01:16:19 +02:00
1cdd11d9f5 main: Fix docked mode button, clang 14 error 2023-08-23 14:26:34 -04:00
ccd163ab2c Merge pull request #11352 from t895/recurse-subfolders
android: Search game directory recursively
2023-08-23 10:20:02 -04:00
182fb83556 android: Set default build variant to mainlineRelWithDebInfo (#11358) 2023-08-23 16:12:39 +02:00
39c8ddcda2 Pre-test opening a stream for audio backends, fall back to null if not suitable. 2023-08-23 08:33:26 +01:00
2c4ebeb51d android: jni: ensure NCAs from loaded filepath are registered in manual content provider 2023-08-22 22:47:25 -04:00
00af46c356 native: Use Docked Mode helper 2023-08-22 22:40:36 -04:00
ce0f1baf51 main: Access by reference
Old Clang is fussy about this.
2023-08-22 22:35:55 -04:00
75f5b3177d config-android: Translate console mode setting
Translates the previous boolean to the enum.
2023-08-22 22:00:28 -04:00
3c45452fae general: Use console mode helper across project 2023-08-22 21:58:23 -04:00
ab862207d7 settings: Add docked mode helper function 2023-08-22 21:58:09 -04:00
7f8335f4ae config(qt): Sanitize docked handheld controller 2023-08-22 16:07:53 -04:00
6ed5b581f0 shared_translation: Define use_docked_mode texts 2023-08-22 16:07:53 -04:00
387ede76d2 general: Convert use_docked_mode to an enumeration
Allows some special interactions with it in the Qt frontend.
2023-08-22 16:07:52 -04:00
8a4cb3f902 shared_widget: Implement radio groups 2023-08-22 16:07:52 -04:00
35b77b9599 android: Search game directory recursively 2023-08-22 15:16:20 -04:00
bc4ad5e62d Merge pull request #11302 from vonchenplus/vulkan_macos
Add macos moltenvk bundle, Add copy moltevk dylib script
2023-08-22 13:10:26 -04:00
0e443dcb05 fix: Added padding to buttons
Some buttons did not have enough padding, now they do!
2023-08-22 10:01:12 -07:00
ef61d129d3 Merge pull request #11303 from lat9nq/screenshots-configurable
yuzu-qt: Add configuration for screenshot resolution
2023-08-22 11:30:25 -04:00
b8bab551a4 Merge pull request #11316 from FernandoS27/stop-premature-christmas-decorating
Shader Recompiler: implement textureGrad 3D
2023-08-22 11:30:08 -04:00
a9f223cd9f Merge pull request #11346 from t895/ktlint-fix
android: lint: Delete generated ktlint folder between builds
2023-08-22 11:30:01 -04:00
87022a4833 Add macos moltenvk bundle, Add copy moltevk dylib script 2023-08-22 10:22:28 +08:00
1bc832c9b1 android: lint: Delete generated ktlint folder between builds
There's a bug in ktlint where it will run into an error if you build the project, delete a source file, and then build again. It will be unable to find the file you deleted and can't recover until these files are deleted. This just deletes those files before every run.
2023-08-21 17:31:13 -04:00
df00da1760 android: Show associated value in home settings (#11272) 2023-08-21 22:25:11 +02:00
9d6ac28999 Merge pull request #11309 from liamwhite/full-xci
file_sys/card_image: support dumps with prepended key area
2023-08-21 16:09:22 -04:00
a921851ba6 Merge pull request #11342 from liamwhite/skyline-4
patch_manager: apply manual HTML patches when present
2023-08-21 16:09:15 -04:00
18c08cee43 Merge pull request #11149 from ameerj/astc-perf-prod
host_shaders: ASTC compute shader optimizations
2023-08-21 16:08:51 -04:00
062113374d android: Use sensor landscape for landscape mode (#11337) 2023-08-21 21:56:12 +02:00
133ff3989b patch_manager: apply manual HTML patches when present 2023-08-21 10:58:23 -04:00
861597eb2e Merge pull request #11284 from liamwhite/nca-release
vfs: expand support for NCA reading
2023-08-21 16:29:04 +02:00
0cd9d51e06 sockets: avoid locking around socket session calls 2023-08-19 23:09:35 -04:00
6a5db5679b Merge pull request #11320 from Kelebek1/mask_depthstencil_clear
Support masked depthstencil clears
2023-08-19 17:28:28 +02:00
f2f99a8c31 Masked depthstencil clears 2023-08-19 03:29:46 +01:00
c03f0b3c89 Shader Recomnpiler: implement textuzreGrad 3D emulation constant propagation 2023-08-18 22:17:02 -04:00
ae1421265a Merge pull request #11278 from Kelebek1/dma_sync
Mark accelerated DMA destination buffers and images as GPU-modified
2023-08-18 09:12:27 -04:00
314d3858a1 Merge pull request #11288 from liamwhite/svc-tick
kernel: remove relative task registration
2023-08-18 09:12:19 -04:00
0383ae1dbf Merge pull request #11310 from vonchenplus/vulkan_format
video_core: Fix vulkan format assert error
2023-08-18 09:12:11 -04:00
1dcb0c2232 video_core: Fix vulkan assert error 2023-08-18 14:40:11 +08:00
8be3a041e0 file_sys/card_image: support dumps with prepended key area 2023-08-17 22:03:47 -04:00
ddedaa8875 Merge pull request #10989 from comex/epipe
sockets: Improve behavior when sending to closed connection
2023-08-17 11:59:47 -04:00
0e3a995bf4 cmake: mark warning disable for gcc 11 (#11301) 2023-08-17 16:03:34 +02:00
6af8cca2c1 uisettings: Add TODO for stretched aspect being ignored 2023-08-16 22:57:19 -04:00
775bf8e215 file_sys: tolerate empty NCA 2023-08-16 16:30:41 -04:00
e28b936950 configure_ui: Silence MSVC warning 2023-08-16 16:28:44 -04:00
6fe51b48e9 yuzu-qt: Screenshots depend more on the graphics settings 2023-08-16 16:12:42 -04:00
96c98d09cb yuzu-qt: Implement unspecified screenshot ratio 2023-08-16 00:18:47 -04:00
76a03e99b6 bootmanager: Remove old path
Causes issues with different selected aspect ratios in graphics.
2023-08-16 00:18:16 -04:00
95409c6859 configure_ui: Update the screenshots data 2023-08-15 23:08:02 -04:00
227950ac99 config: Read the entire screenshots category 2023-08-15 23:07:49 -04:00
bc5ec10498 bootmanager: Consider the default resolution 2023-08-15 22:57:38 -04:00
d9275b7757 yuzu-qt: Enable specifying screenshot resolution 2023-08-15 22:42:28 -04:00
3e28e85468 settings: Add AspectRatio enum, split res scale function 2023-08-15 22:41:50 -04:00
755bcc459b Improve behavior when sending to closed connection
- On Unix, this would previously kill the Yuzu process with SIGPIPE.
  Send MSG_NOSIGNAL to opt out of this.

- Add support for the proper error code in this situation, EPIPE.

- Windows has nonstandard behavior in this situation; translate it to
  the standard behavior.  Kind of pointless, but isn't it nice to be
  correct?
2023-08-15 20:59:57 -04:00
50eee9b218 fssystem: rework for yuzu style 2023-08-15 17:47:40 -04:00
0398b34370 fssystem: reduce overalignment of unbuffered storage operations 2023-08-15 17:47:25 -04:00
86f6b6b7b2 vfs: expand support for NCA reading 2023-08-15 17:47:25 -04:00
a8c4f01f6c Merge pull request #11287 from liamwhite/replaced-bytes
gdbstub: fixup replaced instruction bytes in memory reads
2023-08-15 15:36:14 +02:00
6d665a94ea Merge pull request #11256 from FearlessTobi/revert-10075
Partially Revert "Silence nifm spam"
2023-08-14 16:28:13 -07:00
bbc6b08fc7 Merge pull request #11273 from t895/setup-completion
android: Setup additions
2023-08-14 15:41:35 -07:00
0bd9a4456c kernel: remove relative task registration 2023-08-14 18:12:06 -04:00
fbda084acb gdbstub: fixup replaced instruction bytes in memory reads 2023-08-14 16:33:27 -04:00
2694f81462 Revert "Silence nifm spam"
This reverts commit 4da4ecb1ff.
2023-08-14 21:23:09 +02:00
d5adaeafdf Merge pull request #11271 from t895/settings-tweaks
android: Settings tweaks
2023-08-14 11:44:38 -07:00
58a4c86797 Merge pull request #11282 from ameerj/glasm-xfb
gl_graphics_pipeline: GLASM: Fix transform feedback with multiple buffers
2023-08-14 09:19:20 -04:00
35a77c3bb2 Merge pull request #11283 from ameerj/glasm-pipeline-detection
gl_graphics_pipeline: Fix GLASM storage buffer detection
2023-08-14 09:19:10 -04:00
c1016b68ae Merge pull request #11281 from liamwhite/vi-scale-mode
nvnflinger: add missing scale mode
2023-08-14 09:19:03 -04:00
b30df50076 Merge pull request #11259 from german77/hid
service: hid: Implement functions needed by QLaunch
2023-08-14 09:18:55 -04:00
5afe1367ba Merge pull request #11263 from liamwhite/my-feature-branch
vulkan_device: disable features associated with unloaded extensions
2023-08-14 09:18:47 -04:00
24700af3c2 Merge pull request #11264 from liamwhite/stray-code
ssl_backend_securetransport: remove stray .Code()
2023-08-14 09:18:32 -04:00
f9ef721ca6 gl_graphics_pipeline: Fix GLASM storage buffer detection 2023-08-13 17:06:45 -04:00
c34ed4bbd8 gl_graphics_pipeline: GLASM: Fix transform feedback with multiple buffers 2023-08-13 16:50:01 -04:00
7351884588 nvnflinger: add missing scale mode 2023-08-13 13:57:02 -04:00
5a37b8f2c1 Mark accelerted DMA destination buffers and images as GPU-modified 2023-08-13 02:22:39 +01:00
242ce2a0b3 android: Page forward on setup step completion 2023-08-12 20:21:47 -04:00
8ab3685a39 android: Adjust setup fragment layout
Fixes padding issues in small and large layouts and allows viewpager to reach into system insets.
2023-08-12 17:02:59 -04:00
8bd0521b58 android: Show complete indicator during setup 2023-08-12 16:53:14 -04:00
64ea5522d3 android: Remove redundant option from slider dialog
You can already reset any setting by long pressing the settings item.
2023-08-12 15:45:27 -04:00
798a439eb1 android: Reduce opacity of non-editable settings 2023-08-12 15:42:55 -04:00
786b609151 android: Use string resource for slider value/units 2023-08-12 15:42:54 -04:00
89a2d308c3 android: Display setting value in setting list items 2023-08-12 14:38:46 -04:00
0d4bf53ad9 android: Set switch listener before assigning new value
Previously the switch could have its old listener triggered when recycled.
2023-08-12 01:00:42 -04:00
8b98c4e5a0 ssl_backend_securetransport: remove stray .Code() 2023-08-11 23:32:46 -04:00
7d8f748696 vulkan_device: disable features associated with unloaded extensions 2023-08-11 14:54:12 -04:00
bdd96118d1 service: hid: Implement functions needed by QLaunch 2023-08-11 10:13:21 -06:00
5c25712af9 flatten color_values 2023-08-09 18:45:52 -04:00
0f7220c9c8 flatten encoding_values 2023-08-09 18:38:37 -04:00
71857e889e flatten result vector 2023-08-09 18:34:57 -04:00
70f8ffb787 GetUnquantizedWeightVector 2023-08-09 17:45:39 -04:00
9058486b9b Revert "HACK: Avoid swizzling and reuploading ASTC image every frame"
This reverts commit b18c1fb1bb.
2023-08-06 14:55:05 -04:00
b18c1fb1bb HACK: Avoid swizzling and reuploading ASTC image every frame 2023-08-06 14:54:58 -04:00
913803bf65 Compute Replicate 2023-08-06 14:54:58 -04:00
31a0cff036 minor 2023-08-06 14:54:58 -04:00
b36e645fee undo uint 2023-08-06 14:54:58 -04:00
8ce158bce6 Revert "vulkan dims specialization"
This reverts commit e6243058f2269bd79ac8479d58e55feec2611e9d.
2023-08-06 14:54:58 -04:00
5a78b35b1a vulkan dims specialization 2023-08-06 14:54:58 -04:00
7a0d7e7668 small_block opt 2023-08-06 14:54:58 -04:00
fd2051b401 remove TexelWeightParams 2023-08-06 14:54:57 -04:00
75ac7845ce error/void extent funcs 2023-08-06 14:54:57 -04:00
441b847107 more packing 2023-08-06 14:54:57 -04:00
f2cf81e0b6 Revert "uint result index"
This reverts commit 0e978786b5a8e7382005d8b1e16cfa12f3eeb775.
2023-08-06 14:54:57 -04:00
f41fb3ec0b Revert "bfe instead of mod"
This reverts commit 86006a3b09e8a8c17d2ade61be76736a79e3f58a.
2023-08-06 14:54:57 -04:00
553dd3e120 Revert "global endpoints"
This reverts commit d8f5bfd1df2b7469ef6abcee182aa110602d1751.
2023-08-06 14:54:57 -04:00
c077e467c4 global endpoints 2023-08-06 14:54:57 -04:00
5c16559694 bfe instead of mod 2023-08-06 14:54:57 -04:00
6b0b584eba uint result index 2023-08-06 14:54:57 -04:00
05ee37a1f0 amd opts 2023-08-06 14:54:57 -04:00
3494fce864 gl 2023-08-06 14:54:57 -04:00
5248fa926d const, pack result_vector and replicate tables,
undo amd opts
2023-08-06 14:54:57 -04:00
998246efc2 minor redundancy cleanup 2023-08-06 14:54:57 -04:00
d17a51bc59 extractbits robustness 2023-08-06 14:54:57 -04:00
0078e5a338 reuse vectors memory 2023-08-06 14:54:57 -04:00
b8ca47e094 EncodingData pack 2023-08-06 14:54:57 -04:00
27c8bb9615 flattening 2023-08-06 14:54:57 -04:00
ac09cc3504 weights refactor 2023-08-06 14:54:57 -04:00
6ff65abd62 params.max_weight 2023-08-06 14:54:57 -04:00
e0c59c7b0b skip bits 2023-08-06 14:54:57 -04:00
7ef879b296 restrict 2023-08-06 14:54:57 -04:00
320 changed files with 13653 additions and 4842 deletions

View File

@ -49,7 +49,7 @@ option(YUZU_TESTS "Compile tests" "${BUILD_TESTING}")
option(YUZU_USE_PRECOMPILED_HEADERS "Use precompiled headers" ON) option(YUZU_USE_PRECOMPILED_HEADERS "Use precompiled headers" ON)
cmake_dependent_option(YUZU_ROOM "Compile LDN room server" ON "NOT ANDROID" OFF) CMAKE_DEPENDENT_OPTION(YUZU_ROOM "Compile LDN room server" ON "NOT ANDROID" OFF)
CMAKE_DEPENDENT_OPTION(YUZU_CRASH_DUMPS "Compile Windows crash dump (Minidump) support" OFF "WIN32" OFF) CMAKE_DEPENDENT_OPTION(YUZU_CRASH_DUMPS "Compile Windows crash dump (Minidump) support" OFF "WIN32" OFF)
@ -63,6 +63,8 @@ option(YUZU_DOWNLOAD_TIME_ZONE_DATA "Always download time zone binaries" OFF)
CMAKE_DEPENDENT_OPTION(YUZU_USE_FASTER_LD "Check if a faster linker is available" ON "NOT WIN32" OFF) CMAKE_DEPENDENT_OPTION(YUZU_USE_FASTER_LD "Check if a faster linker is available" ON "NOT WIN32" OFF)
CMAKE_DEPENDENT_OPTION(USE_SYSTEM_MOLTENVK "Use the system MoltenVK lib (instead of the bundled one)" OFF "APPLE" OFF)
set(DEFAULT_ENABLE_OPENSSL ON) set(DEFAULT_ENABLE_OPENSSL ON)
if (ANDROID OR WIN32 OR APPLE) if (ANDROID OR WIN32 OR APPLE)
# - Windows defaults to the Schannel backend. # - Windows defaults to the Schannel backend.
@ -522,7 +524,7 @@ if (ENABLE_SDL2)
if (YUZU_USE_BUNDLED_SDL2) if (YUZU_USE_BUNDLED_SDL2)
# Detect toolchain and platform # Detect toolchain and platform
if ((MSVC_VERSION GREATER_EQUAL 1920 AND MSVC_VERSION LESS 1940) AND ARCHITECTURE_x86_64) if ((MSVC_VERSION GREATER_EQUAL 1920 AND MSVC_VERSION LESS 1940) AND ARCHITECTURE_x86_64)
set(SDL2_VER "SDL2-2.28.1") set(SDL2_VER "SDL2-2.28.2")
else() else()
message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable YUZU_USE_BUNDLED_SDL2 and provide your own.") message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable YUZU_USE_BUNDLED_SDL2 and provide your own.")
endif() endif()

View File

@ -36,3 +36,21 @@ endif()
message(STATUS "Using bundled binaries at ${prefix}") message(STATUS "Using bundled binaries at ${prefix}")
set(${prefix_var} "${prefix}" PARENT_SCOPE) set(${prefix_var} "${prefix}" PARENT_SCOPE)
endfunction() endfunction()
function(download_moltenvk_external platform version)
set(MOLTENVK_DIR "${CMAKE_BINARY_DIR}/externals/MoltenVK")
set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar")
if (NOT EXISTS ${MOLTENVK_DIR})
if (NOT EXISTS ${MOLTENVK_TAR})
file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/${version}/MoltenVK-${platform}.tar
${MOLTENVK_TAR} SHOW_PROGRESS)
endif()
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${MOLTENVK_TAR}"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals")
endif()
# Add the MoltenVK library path to the prefix so find_library can locate it.
list(APPEND CMAKE_PREFIX_PATH "${MOLTENVK_DIR}/MoltenVK/dylib/${platform}")
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE)
endfunction()

View File

@ -1,3 +1,11 @@
| Pull Request | Commit | Title | Author | Merged? |
|----|----|----|----|----|
End of merge log. You can find the original README.md below the break.
-----
<!-- <!--
SPDX-FileCopyrightText: 2018 yuzu Emulator Project SPDX-FileCopyrightText: 2018 yuzu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later SPDX-License-Identifier: GPL-2.0-or-later

View File

@ -78,6 +78,11 @@ QPushButton#buttonRefreshDevices {
max-height: 21px; max-height: 21px;
} }
QPushButton#button_reset_defaults {
min-width: 57px;
padding: 4px 8px;
}
QWidget#bottomPerGameInput, QWidget#bottomPerGameInput,
QWidget#topControllerApplet, QWidget#topControllerApplet,
QWidget#bottomControllerApplet, QWidget#bottomControllerApplet,

View File

@ -2228,6 +2228,10 @@ QPushButton#buttonRefreshDevices {
padding: 0px 0px; padding: 0px 0px;
} }
QPushButton#button_reset_defaults {
padding: 3px 6px;
}
QSpinBox#spinboxLStickRange, QSpinBox#spinboxLStickRange,
QSpinBox#spinboxRStickRange, QSpinBox#spinboxRStickRange,
QSpinBox#vibrationSpinPlayer1, QSpinBox#vibrationSpinPlayer1,

View File

@ -42,6 +42,11 @@ endif()
# mbedtls # mbedtls
add_subdirectory(mbedtls) add_subdirectory(mbedtls)
target_include_directories(mbedtls PUBLIC ./mbedtls/include) target_include_directories(mbedtls PUBLIC ./mbedtls/include)
if (NOT MSVC)
target_compile_options(mbedcrypto PRIVATE
-Wno-unused-but-set-variable
-Wno-string-concatenation)
endif()
# MicroProfile # MicroProfile
add_library(microprofile INTERFACE) add_library(microprofile INTERFACE)
@ -94,6 +99,12 @@ if (ENABLE_CUBEB AND NOT TARGET cubeb::cubeb)
set(BUILD_TOOLS OFF) set(BUILD_TOOLS OFF)
add_subdirectory(cubeb) add_subdirectory(cubeb)
add_library(cubeb::cubeb ALIAS cubeb) add_library(cubeb::cubeb ALIAS cubeb)
if (NOT MSVC)
if (TARGET speex)
target_compile_options(speex PRIVATE -Wno-sign-compare)
endif()
target_compile_options(cubeb PRIVATE -Wno-implicit-const-int-float-conversion)
endif()
endif() endif()
# DiscordRPC # DiscordRPC
@ -151,6 +162,9 @@ endif()
if (NOT TARGET LLVM::Demangle) if (NOT TARGET LLVM::Demangle)
add_library(demangle demangle/ItaniumDemangle.cpp) add_library(demangle demangle/ItaniumDemangle.cpp)
target_include_directories(demangle PUBLIC ./demangle) target_include_directories(demangle PUBLIC ./demangle)
if (NOT MSVC)
target_compile_options(demangle PRIVATE -Wno-deprecated-declarations) # std::is_pod
endif()
add_library(LLVM::Demangle ALIAS demangle) add_library(LLVM::Demangle ALIAS demangle)
endif() endif()

2
externals/SDL vendored

View File

@ -164,7 +164,7 @@ if (NOT WIN32 AND NOT ANDROID)
--enable-decoder=h264 --enable-decoder=h264
--enable-decoder=vp8 --enable-decoder=vp8
--enable-decoder=vp9 --enable-decoder=vp9
--enable-filter=yadif --enable-filter=yadif,scale
--cc="${FFmpeg_CC}" --cc="${FFmpeg_CC}"
--cxx="${FFmpeg_CXX}" --cxx="${FFmpeg_CXX}"
${FFmpeg_HWACCEL_FLAGS} ${FFmpeg_HWACCEL_FLAGS}
@ -254,7 +254,7 @@ elseif(ANDROID)
set(FFmpeg_INCLUDE_DIR "${FFmpeg_INCLUDE_DIR}" PARENT_SCOPE) set(FFmpeg_INCLUDE_DIR "${FFmpeg_INCLUDE_DIR}" PARENT_SCOPE)
elseif(WIN32) elseif(WIN32)
# Use yuzu FFmpeg binaries # Use yuzu FFmpeg binaries
set(FFmpeg_EXT_NAME "ffmpeg-5.1.3") set(FFmpeg_EXT_NAME "ffmpeg-6.0")
set(FFmpeg_PATH "${CMAKE_BINARY_DIR}/externals/${FFmpeg_EXT_NAME}") set(FFmpeg_PATH "${CMAKE_BINARY_DIR}/externals/${FFmpeg_EXT_NAME}")
download_bundled_external("ffmpeg/" ${FFmpeg_EXT_NAME} "") download_bundled_external("ffmpeg/" ${FFmpeg_EXT_NAME} "")
set(FFmpeg_FOUND YES) set(FFmpeg_FOUND YES)

View File

@ -114,16 +114,19 @@ else()
-Wno-attributes -Wno-attributes
-Wno-invalid-offsetof -Wno-invalid-offsetof
-Wno-unused-parameter -Wno-unused-parameter
$<$<CXX_COMPILER_ID:Clang>:-Wno-braced-scalar-init>
$<$<CXX_COMPILER_ID:Clang>:-Wno-unused-private-field>
$<$<CXX_COMPILER_ID:Clang>:-Werror=shadow-uncaptured-local>
$<$<CXX_COMPILER_ID:Clang>:-Werror=implicit-fallthrough>
$<$<CXX_COMPILER_ID:Clang>:-Werror=type-limits>
$<$<CXX_COMPILER_ID:AppleClang>:-Wno-braced-scalar-init>
$<$<CXX_COMPILER_ID:AppleClang>:-Wno-unused-private-field>
) )
if (CMAKE_CXX_COMPILER_ID MATCHES Clang) # Clang or AppleClang
add_compile_options(
-Wno-braced-scalar-init
-Wno-unused-private-field
-Wno-nullability-completeness
-Werror=shadow-uncaptured-local
-Werror=implicit-fallthrough
-Werror=type-limits
)
endif()
if (ARCHITECTURE_x86_64) if (ARCHITECTURE_x86_64)
add_compile_options("-mcx16") add_compile_options("-mcx16")
add_compile_options("-fwrapv") add_compile_options("-fwrapv")
@ -134,7 +137,7 @@ else()
endif() endif()
# GCC bugs # GCC bugs
if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "12" AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "11" AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# These diagnostics would be great if they worked, but are just completely broken # These diagnostics would be great if they worked, but are just completely broken
# and produce bogus errors on external libraries like fmt. # and produce bogus errors on external libraries like fmt.
add_compile_options( add_compile_options(

View File

@ -95,6 +95,7 @@ android {
// builds a release build that doesn't need signing // builds a release build that doesn't need signing
// Attaches 'debug' suffix to version and package name, allowing installation alongside the release build. // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
register("relWithDebInfo") { register("relWithDebInfo") {
isDefault = true
resValue("string", "app_name_suffixed", "yuzu Debug Release") resValue("string", "app_name_suffixed", "yuzu Debug Release")
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
isMinifyEnabled = true isMinifyEnabled = true
@ -122,6 +123,7 @@ android {
flavorDimensions.add("version") flavorDimensions.add("version")
productFlavors { productFlavors {
create("mainline") { create("mainline") {
isDefault = true
dimension = "version" dimension = "version"
buildConfigField("Boolean", "PREMIUM", "false") buildConfigField("Boolean", "PREMIUM", "false")
} }
@ -160,6 +162,11 @@ android {
} }
} }
tasks.create<Delete>("ktlintReset") {
delete(File(buildDir.path + File.separator + "intermediates/ktLint"))
}
tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset")
tasks.getByPath("preBuild").dependsOn("ktlintCheck") tasks.getByPath("preBuild").dependsOn("ktlintCheck")
ktlint { ktlint {

View File

@ -25,6 +25,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
android:hasFragileUserData="false" android:hasFragileUserData="false"
android:supportsRtl="true" android:supportsRtl="true"
android:isGame="true" android:isGame="true"
android:appCategory="game"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:banner="@drawable/tv_banner" android:banner="@drawable/tv_banner"
android:extractNativeLibs="true" android:extractNativeLibs="true"
@ -55,7 +56,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
android:name="org.yuzu.yuzu_emu.activities.EmulationActivity" android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
android:theme="@style/Theme.Yuzu.Main" android:theme="@style/Theme.Yuzu.Main"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="userLandscape"
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:exported="true"> android:exported="true">
@ -66,6 +66,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
<data android:mimeType="application/octet-stream" /> <data android:mimeType="application/octet-stream" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="application/octet-stream"
android:scheme="content"/>
</intent-filter>
<meta-data <meta-data
android:name="android.nfc.action.TECH_DISCOVERED" android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" /> android:resource="@xml/nfc_tech_filter" />

View File

@ -22,9 +22,7 @@ import org.yuzu.yuzu_emu.utils.FileUtil.exists
import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize
import org.yuzu.yuzu_emu.utils.FileUtil.isDirectory import org.yuzu.yuzu_emu.utils.FileUtil.isDirectory
import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri
import org.yuzu.yuzu_emu.utils.Log.error import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.Log.verbose
import org.yuzu.yuzu_emu.utils.Log.warning
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
/** /**
@ -219,10 +217,6 @@ object NativeLibrary {
external fun reloadSettings() external fun reloadSettings()
external fun getUserSetting(gameID: String?, Section: String?, Key: String?): String?
external fun setUserSetting(gameID: String?, Section: String?, Key: String?, Value: String?)
external fun initGameIni(gameID: String?) external fun initGameIni(gameID: String?)
/** /**
@ -413,14 +407,17 @@ object NativeLibrary {
details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) } details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) }
) )
} }
CoreError.ErrorSavestate -> { CoreError.ErrorSavestate -> {
title = emulationActivity.getString(R.string.save_load_error) title = emulationActivity.getString(R.string.save_load_error)
message = details message = details
} }
CoreError.ErrorUnknown -> { CoreError.ErrorUnknown -> {
title = emulationActivity.getString(R.string.fatal_error) title = emulationActivity.getString(R.string.fatal_error)
message = emulationActivity.getString(R.string.fatal_error_message) message = emulationActivity.getString(R.string.fatal_error_message)
} }
else -> { else -> {
return true return true
} }
@ -454,6 +451,7 @@ object NativeLibrary {
captionId = R.string.loader_error_video_core captionId = R.string.loader_error_video_core
descriptionId = R.string.loader_error_video_core_description descriptionId = R.string.loader_error_video_core_description
} }
else -> { else -> {
captionId = R.string.loader_error_encrypted captionId = R.string.loader_error_encrypted
descriptionId = R.string.loader_error_encrypted_roms_description descriptionId = R.string.loader_error_encrypted_roms_description
@ -465,7 +463,7 @@ object NativeLibrary {
val emulationActivity = sEmulationActivity.get() val emulationActivity = sEmulationActivity.get()
if (emulationActivity == null) { if (emulationActivity == null) {
warning("[NativeLibrary] EmulationActivity is null, can't exit.") Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.")
return return
} }
@ -490,15 +488,27 @@ object NativeLibrary {
} }
fun setEmulationActivity(emulationActivity: EmulationActivity?) { fun setEmulationActivity(emulationActivity: EmulationActivity?) {
verbose("[NativeLibrary] Registering EmulationActivity.") Log.verbose("[NativeLibrary] Registering EmulationActivity.")
sEmulationActivity = WeakReference(emulationActivity) sEmulationActivity = WeakReference(emulationActivity)
} }
fun clearEmulationActivity() { fun clearEmulationActivity() {
verbose("[NativeLibrary] Unregistering EmulationActivity.") Log.verbose("[NativeLibrary] Unregistering EmulationActivity.")
sEmulationActivity.clear() sEmulationActivity.clear()
} }
@Keep
@JvmStatic
fun onEmulationStarted() {
sEmulationActivity.get()!!.onEmulationStarted()
}
@Keep
@JvmStatic
fun onEmulationStopped(status: Int) {
sEmulationActivity.get()!!.onEmulationStopped(status)
}
/** /**
* Logs the Yuzu version, Android version and, CPU. * Logs the Yuzu version, Android version and, CPU.
*/ */

View File

@ -46,7 +46,7 @@ class YuzuApplication : Application() {
super.onCreate() super.onCreate()
application = this application = this
documentsTree = DocumentsTree() documentsTree = DocumentsTree()
DirectoryInitialization.start(applicationContext) DirectoryInitialization.start()
GpuDriverHelper.initializeDriverParameters(applicationContext) GpuDriverHelper.initializeDriverParameters(applicationContext)
NativeLibrary.logDeviceInfo() NativeLibrary.logDeviceInfo()

View File

@ -42,7 +42,7 @@ import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel import org.yuzu.yuzu_emu.model.EmulationViewModel
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.ControllerMappingHelper import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
import org.yuzu.yuzu_emu.utils.ForegroundService import org.yuzu.yuzu_emu.utils.ForegroundService
@ -72,18 +72,17 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private val actionMute = "ACTION_EMULATOR_MUTE" private val actionMute = "ACTION_EMULATOR_MUTE"
private val actionUnmute = "ACTION_EMULATOR_UNMUTE" private val actionUnmute = "ACTION_EMULATOR_UNMUTE"
private val settingsViewModel: SettingsViewModel by viewModels() private val emulationViewModel: EmulationViewModel by viewModels()
override fun onDestroy() { override fun onDestroy() {
stopForegroundService(this) stopForegroundService(this)
emulationViewModel.clear()
super.onDestroy() super.onDestroy()
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.setTheme(this) ThemeHelper.setTheme(this)
settingsViewModel.settings.loadSettings()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityEmulationBinding.inflate(layoutInflater) binding = ActivityEmulationBinding.inflate(layoutInflater)
@ -91,9 +90,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
val navHostFragment = val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
val navController = navHostFragment.navController navHostFragment.navController.setGraph(R.navigation.emulation_navigation, intent.extras)
navController
.setGraph(R.navigation.emulation_navigation, intent.extras)
isActivityRecreated = savedInstanceState != null isActivityRecreated = savedInstanceState != null
@ -424,6 +421,16 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
} }
} }
fun onEmulationStarted() {
emulationViewModel.setEmulationStarted(true)
}
fun onEmulationStopped(status: Int) {
if (status == 0) {
finish()
}
}
private fun startMotionSensorListener() { private fun startMotionSensorListener() {
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)

View File

@ -3,8 +3,8 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.graphics.Bitmap import android.content.Intent
import android.graphics.BitmapFactory import android.graphics.drawable.BitmapDrawable
import android.net.Uri import android.net.Uri
import android.text.TextUtils import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
@ -13,25 +13,26 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder
import org.yuzu.yuzu_emu.databinding.CardGameBinding import org.yuzu.yuzu_emu.databinding.CardGameBinding
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.utils.GameIconUtils
class GameAdapter(private val activity: AppCompatActivity) : class GameAdapter(private val activity: AppCompatActivity) :
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
@ -82,6 +83,21 @@ class GameAdapter(private val activity: AppCompatActivity) :
) )
.apply() .apply()
val openIntent = Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = Uri.parse(holder.game.path)
}
val shortcut = ShortcutInfoCompat.Builder(YuzuApplication.appContext, holder.game.path)
.setShortLabel(holder.game.title)
.setIcon(
IconCompat.createWithBitmap(
(holder.binding.imageGameScreen.drawable as BitmapDrawable).bitmap
)
)
.setIntent(openIntent)
.build()
ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut)
val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game) val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game)
view.findNavController().navigate(action) view.findNavController().navigate(action)
} }
@ -98,12 +114,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
this.game = game this.game = game
binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
activity.lifecycleScope.launch { GameIconUtils.loadGameIcon(game, binding.imageGameScreen)
val bitmap = decodeGameIcon(game.path)
binding.imageGameScreen.load(bitmap) {
error(R.drawable.default_icon)
}
}
binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ") binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ")
@ -126,14 +137,4 @@ class GameAdapter(private val activity: AppCompatActivity) :
return oldItem == newItem return oldItem == newItem
} }
} }
private fun decodeGameIcon(uri: String): Bitmap? {
val data = NativeLibrary.getIcon(uri)
return BitmapFactory.decodeByteArray(
data,
0,
data.size,
BitmapFactory.Options()
)
}
} }

View File

@ -3,19 +3,25 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.model.HomeSetting import org.yuzu.yuzu_emu.model.HomeSetting
class HomeSettingAdapter(private val activity: AppCompatActivity, var options: List<HomeSetting>) : class HomeSettingAdapter(
private val activity: AppCompatActivity,
private val viewLifecycle: LifecycleOwner,
var options: List<HomeSetting>
) :
RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(), RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(),
View.OnClickListener { View.OnClickListener {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder {
@ -39,8 +45,8 @@ class HomeSettingAdapter(private val activity: AppCompatActivity, var options: L
holder.option.onClick.invoke() holder.option.onClick.invoke()
} else { } else {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
holder.option.disabledTitleId, titleId = holder.option.disabledTitleId,
holder.option.disabledMessageId descriptionId = holder.option.disabledMessageId
).show(activity.supportFragmentManager, MessageDialogFragment.TAG) ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
} }
} }
@ -79,6 +85,22 @@ class HomeSettingAdapter(private val activity: AppCompatActivity, var options: L
binding.optionDescription.alpha = 0.5f binding.optionDescription.alpha = 0.5f
binding.optionIcon.alpha = 0.5f binding.optionIcon.alpha = 0.5f
} }
option.details.observe(viewLifecycle) { updateOptionDetails(it) }
binding.optionDetail.postDelayed(
{
binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE
binding.optionDetail.isSelected = true
},
3000
)
}
private fun updateOptionDetails(detailString: String) {
if (detailString.isNotEmpty()) {
binding.optionDetail.text = detailString
binding.optionDetail.visibility = View.VISIBLE
}
} }
} }
} }

View File

@ -49,6 +49,7 @@ class LicenseAdapter(private val activity: AppCompatActivity, var licenses: List
val context = YuzuApplication.appContext val context = YuzuApplication.appContext
binding.textSettingName.text = context.getString(license.titleId) binding.textSettingName.text = context.getString(license.titleId)
binding.textSettingDescription.text = context.getString(license.descriptionId) binding.textSettingDescription.text = context.getString(license.descriptionId)
binding.textSettingValue.visibility = View.GONE
} }
} }
} }

View File

@ -5,13 +5,19 @@ package org.yuzu.yuzu_emu.adapters
import android.text.Html import android.text.Html
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import org.yuzu.yuzu_emu.databinding.PageSetupBinding import org.yuzu.yuzu_emu.databinding.PageSetupBinding
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.SetupCallback
import org.yuzu.yuzu_emu.model.SetupPage import org.yuzu.yuzu_emu.model.SetupPage
import org.yuzu.yuzu_emu.model.StepState
import org.yuzu.yuzu_emu.utils.ViewUtils
class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) : class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) :
RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() { RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() {
@ -26,7 +32,7 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
holder.bind(pages[position]) holder.bind(pages[position])
inner class SetupPageViewHolder(val binding: PageSetupBinding) : inner class SetupPageViewHolder(val binding: PageSetupBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root), SetupCallback {
lateinit var page: SetupPage lateinit var page: SetupPage
init { init {
@ -35,6 +41,12 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
fun bind(page: SetupPage) { fun bind(page: SetupPage) {
this.page = page this.page = page
if (page.stepCompleted.invoke() == StepState.COMPLETE) {
binding.buttonAction.visibility = View.INVISIBLE
binding.textConfirmation.visibility = View.VISIBLE
}
binding.icon.setImageDrawable( binding.icon.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(
activity.resources, activity.resources,
@ -62,9 +74,15 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
MaterialButton.ICON_GRAVITY_END MaterialButton.ICON_GRAVITY_END
} }
setOnClickListener { setOnClickListener {
page.buttonAction.invoke() page.buttonAction.invoke(this@SetupPageViewHolder)
} }
} }
} }
override fun onStepCompleted() {
ViewUtils.hideView(binding.buttonAction, 200)
ViewUtils.showView(binding.textConfirmation, 200)
ViewModelProvider(activity)[HomeViewModel::class.java].setShouldPageForward(true)
}
} }
} }

View File

@ -4,43 +4,43 @@
package org.yuzu.yuzu_emu.disk_shader_cache package org.yuzu.yuzu_emu.disk_shader_cache
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.lifecycle.ViewModelProvider
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.disk_shader_cache.ui.ShaderProgressDialogFragment import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.model.EmulationViewModel
import org.yuzu.yuzu_emu.utils.Log
@Keep @Keep
object DiskShaderCacheProgress { object DiskShaderCacheProgress {
val finishLock = Object() private lateinit var emulationViewModel: EmulationViewModel
private lateinit var fragment: ShaderProgressDialogFragment
private fun prepareDialog() { private fun prepareViewModel() {
val emulationActivity = NativeLibrary.sEmulationActivity.get()!! emulationViewModel =
emulationActivity.runOnUiThread { ViewModelProvider(
fragment = ShaderProgressDialogFragment.newInstance( NativeLibrary.sEmulationActivity.get() as EmulationActivity
emulationActivity.getString(R.string.loading), )[EmulationViewModel::class.java]
emulationActivity.getString(R.string.preparing_shaders)
)
fragment.show(
emulationActivity.supportFragmentManager,
ShaderProgressDialogFragment.TAG
)
}
synchronized(finishLock) { finishLock.wait() }
} }
@JvmStatic @JvmStatic
fun loadProgress(stage: Int, progress: Int, max: Int) { fun loadProgress(stage: Int, progress: Int, max: Int) {
val emulationActivity = NativeLibrary.sEmulationActivity.get() val emulationActivity = NativeLibrary.sEmulationActivity.get()
?: error("[DiskShaderCacheProgress] EmulationActivity not present") if (emulationActivity == null) {
Log.error("[DiskShaderCacheProgress] EmulationActivity not present")
return
}
emulationActivity.runOnUiThread {
when (LoadCallbackStage.values()[stage]) { when (LoadCallbackStage.values()[stage]) {
LoadCallbackStage.Prepare -> prepareDialog() LoadCallbackStage.Prepare -> prepareViewModel()
LoadCallbackStage.Build -> fragment.onUpdateProgress( LoadCallbackStage.Build -> emulationViewModel.updateProgress(
emulationActivity.getString(R.string.building_shaders), emulationActivity.getString(R.string.building_shaders),
progress, progress,
max max
) )
LoadCallbackStage.Complete -> fragment.dismiss()
LoadCallbackStage.Complete -> {}
}
} }
} }

View File

@ -1,31 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.disk_shader_cache
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class ShaderProgressViewModel : ViewModel() {
private val _progress = MutableLiveData(0)
val progress: LiveData<Int> get() = _progress
private val _max = MutableLiveData(0)
val max: LiveData<Int> get() = _max
private val _message = MutableLiveData("")
val message: LiveData<String> get() = _message
fun setProgress(progress: Int) {
_progress.postValue(progress)
}
fun setMax(max: Int) {
_max.postValue(max)
}
fun setMessage(msg: String) {
_message.postValue(msg)
}
}

View File

@ -1,103 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.disk_shader_cache.ui
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.disk_shader_cache.DiskShaderCacheProgress
import org.yuzu.yuzu_emu.disk_shader_cache.ShaderProgressViewModel
class ShaderProgressDialogFragment : DialogFragment() {
private var _binding: DialogProgressBarBinding? = null
private val binding get() = _binding!!
private lateinit var alertDialog: AlertDialog
private lateinit var shaderProgressViewModel: ShaderProgressViewModel
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DialogProgressBarBinding.inflate(layoutInflater)
shaderProgressViewModel =
ViewModelProvider(requireActivity())[ShaderProgressViewModel::class.java]
val title = requireArguments().getString(TITLE)
val message = requireArguments().getString(MESSAGE)
isCancelable = false
alertDialog = MaterialAlertDialogBuilder(requireActivity())
.setView(binding.root)
.setTitle(title)
.setMessage(message)
.create()
return alertDialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shaderProgressViewModel.progress.observe(viewLifecycleOwner) { progress ->
binding.progressBar.progress = progress
setUpdateText()
}
shaderProgressViewModel.max.observe(viewLifecycleOwner) { max ->
binding.progressBar.max = max
setUpdateText()
}
shaderProgressViewModel.message.observe(viewLifecycleOwner) { msg ->
alertDialog.setMessage(msg)
}
synchronized(DiskShaderCacheProgress.finishLock) {
DiskShaderCacheProgress.finishLock.notifyAll()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
fun onUpdateProgress(msg: String, progress: Int, max: Int) {
shaderProgressViewModel.setProgress(progress)
shaderProgressViewModel.setMax(max)
shaderProgressViewModel.setMessage(msg)
}
private fun setUpdateText() {
binding.progressText.text = String.format(
"%d/%d",
shaderProgressViewModel.progress.value,
shaderProgressViewModel.max.value
)
}
companion object {
const val TAG = "ProgressDialogFragment"
const val TITLE = "title"
const val MESSAGE = "message"
fun newInstance(title: String, message: String): ShaderProgressDialogFragment {
val frag = ShaderProgressDialogFragment()
val args = Bundle()
args.putString(TITLE, title)
args.putString(MESSAGE, message)
frag.arguments = args
return frag
}
}
}

View File

@ -4,5 +4,7 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractBooleanSetting : AbstractSetting { interface AbstractBooleanSetting : AbstractSetting {
var boolean: Boolean val boolean: Boolean
fun setBoolean(value: Boolean)
} }

View File

@ -3,8 +3,8 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import androidx.lifecycle.ViewModel interface AbstractByteSetting : AbstractSetting {
val byte: Byte
class SettingsViewModel : ViewModel() { fun setByte(value: Byte)
val settings = Settings()
} }

View File

@ -4,5 +4,7 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractFloatSetting : AbstractSetting { interface AbstractFloatSetting : AbstractSetting {
var float: Float val float: Float
fun setFloat(value: Float)
} }

View File

@ -4,5 +4,7 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractIntSetting : AbstractSetting { interface AbstractIntSetting : AbstractSetting {
var int: Int val int: Int
fun setInt(value: Int)
} }

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model
interface AbstractLongSetting : AbstractSetting {
val long: Long
fun setLong(value: Long)
}

View File

@ -3,10 +3,22 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
interface AbstractSetting { interface AbstractSetting {
val key: String? val key: String
val section: String? val category: Settings.Category
val isRuntimeEditable: Boolean
val valueAsString: String
val defaultValue: Any val defaultValue: Any
val androidDefault: Any?
get() = null
val valueAsString: String
get() = ""
val isRuntimeModifiable: Boolean
get() = NativeConfig.getIsRuntimeModifiable(key)
val pairedSettingKey: String
get() = NativeConfig.getPairedSettingKey(key)
fun reset()
} }

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model
interface AbstractShortSetting : AbstractSetting {
val short: Short
fun setShort(value: Short)
}

View File

@ -4,5 +4,7 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractStringSetting : AbstractSetting { interface AbstractStringSetting : AbstractSetting {
var string: String val string: String
fun setString(value: String)
} }

View File

@ -3,41 +3,37 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class BooleanSetting( enum class BooleanSetting(
override val key: String, override val key: String,
override val section: String, override val category: Settings.Category,
override val defaultValue: Boolean override val androidDefault: Boolean? = null
) : AbstractBooleanSetting { ) : AbstractBooleanSetting {
CPU_DEBUG_MODE("cpu_debug_mode", Settings.SECTION_CPU, false), CPU_DEBUG_MODE("cpu_debug_mode", Settings.Category.Cpu),
FASTMEM("cpuopt_fastmem", Settings.SECTION_CPU, true), FASTMEM("cpuopt_fastmem", Settings.Category.Cpu),
FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.SECTION_CPU, true), FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.Category.Cpu),
PICTURE_IN_PICTURE("picture_in_picture", Settings.SECTION_GENERAL, true), RENDERER_USE_SPEED_LIMIT("use_speed_limit", Settings.Category.Core),
USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false); USE_DOCKED_MODE("use_docked_mode", Settings.Category.System, false),
RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache", Settings.Category.Renderer),
RENDERER_FORCE_MAX_CLOCK("force_max_clock", Settings.Category.Renderer),
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders", Settings.Category.Renderer),
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing", Settings.Category.Renderer, false),
RENDERER_DEBUG("debug", Settings.Category.Renderer),
PICTURE_IN_PICTURE("picture_in_picture", Settings.Category.Android),
USE_CUSTOM_RTC("custom_rtc_enabled", Settings.Category.System);
override var boolean: Boolean = defaultValue override val boolean: Boolean
get() = NativeConfig.getBoolean(key, false)
override fun setBoolean(value: Boolean) = NativeConfig.setBoolean(key, value)
override val defaultValue: Boolean by lazy {
androidDefault ?: NativeConfig.getBoolean(key, true)
}
override val valueAsString: String override val valueAsString: String
get() = boolean.toString() get() = if (boolean) "1" else "0"
override val isRuntimeEditable: Boolean override fun reset() = NativeConfig.setBoolean(key, defaultValue)
get() {
for (setting in NOT_RUNTIME_EDITABLE) {
if (setting == this) {
return false
}
}
return true
}
companion object {
private val NOT_RUNTIME_EDITABLE = listOf(
PICTURE_IN_PICTURE,
USE_CUSTOM_RTC
)
fun from(key: String): BooleanSetting? =
BooleanSetting.values().firstOrNull { it.key == key }
fun clear() = BooleanSetting.values().forEach { it.boolean = it.defaultValue }
}
} }

View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class ByteSetting(
override val key: String,
override val category: Settings.Category
) : AbstractByteSetting {
AUDIO_VOLUME("volume", Settings.Category.Audio);
override val byte: Byte
get() = NativeConfig.getByte(key, false)
override fun setByte(value: Byte) = NativeConfig.setByte(key, value)
override val defaultValue: Byte by lazy { NativeConfig.getByte(key, true) }
override val valueAsString: String
get() = byte.toString()
override fun reset() = NativeConfig.setByte(key, defaultValue)
}

View File

@ -3,34 +3,24 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class FloatSetting( enum class FloatSetting(
override val key: String, override val key: String,
override val section: String, override val category: Settings.Category
override val defaultValue: Float
) : AbstractFloatSetting { ) : AbstractFloatSetting {
// No float settings currently exist // No float settings currently exist
EMPTY_SETTING("", "", 0f); EMPTY_SETTING("", Settings.Category.UiGeneral);
override var float: Float = defaultValue override val float: Float
get() = NativeConfig.getFloat(key, false)
override fun setFloat(value: Float) = NativeConfig.setFloat(key, value)
override val defaultValue: Float by lazy { NativeConfig.getFloat(key, true) }
override val valueAsString: String override val valueAsString: String
get() = float.toString() get() = float.toString()
override val isRuntimeEditable: Boolean override fun reset() = NativeConfig.setFloat(key, defaultValue)
get() {
for (setting in NOT_RUNTIME_EDITABLE) {
if (setting == this) {
return false
}
}
return true
}
companion object {
private val NOT_RUNTIME_EDITABLE = emptyList<FloatSetting>()
fun from(key: String): FloatSetting? = FloatSetting.values().firstOrNull { it.key == key }
fun clear() = FloatSetting.values().forEach { it.float = it.defaultValue }
}
} }

View File

@ -3,139 +3,37 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class IntSetting( enum class IntSetting(
override val key: String, override val key: String,
override val section: String, override val category: Settings.Category,
override val defaultValue: Int override val androidDefault: Int? = null
) : AbstractIntSetting { ) : AbstractIntSetting {
RENDERER_USE_SPEED_LIMIT( CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu),
"use_speed_limit", REGION_INDEX("region_index", Settings.Category.System),
Settings.SECTION_RENDERER, LANGUAGE_INDEX("language_index", Settings.Category.System),
1 RENDERER_BACKEND("backend", Settings.Category.Renderer),
), RENDERER_ACCURACY("gpu_accuracy", Settings.Category.Renderer, 0),
USE_DOCKED_MODE( RENDERER_RESOLUTION("resolution_setup", Settings.Category.Renderer),
"use_docked_mode", RENDERER_VSYNC("use_vsync", Settings.Category.Renderer),
Settings.SECTION_SYSTEM, RENDERER_SCALING_FILTER("scaling_filter", Settings.Category.Renderer),
0 RENDERER_ANTI_ALIASING("anti_aliasing", Settings.Category.Renderer),
), RENDERER_SCREEN_LAYOUT("screen_layout", Settings.Category.Android),
RENDERER_USE_DISK_SHADER_CACHE( RENDERER_ASPECT_RATIO("aspect_ratio", Settings.Category.Renderer),
"use_disk_shader_cache", AUDIO_OUTPUT_ENGINE("output_engine", Settings.Category.Audio);
Settings.SECTION_RENDERER,
1
),
RENDERER_FORCE_MAX_CLOCK(
"force_max_clock",
Settings.SECTION_RENDERER,
0
),
RENDERER_ASYNCHRONOUS_SHADERS(
"use_asynchronous_shaders",
Settings.SECTION_RENDERER,
0
),
RENDERER_REACTIVE_FLUSHING(
"use_reactive_flushing",
Settings.SECTION_RENDERER,
0
),
RENDERER_DEBUG(
"debug",
Settings.SECTION_RENDERER,
0
),
RENDERER_SPEED_LIMIT(
"speed_limit",
Settings.SECTION_RENDERER,
100
),
CPU_ACCURACY(
"cpu_accuracy",
Settings.SECTION_CPU,
0
),
REGION_INDEX(
"region_index",
Settings.SECTION_SYSTEM,
-1
),
LANGUAGE_INDEX(
"language_index",
Settings.SECTION_SYSTEM,
1
),
RENDERER_BACKEND(
"backend",
Settings.SECTION_RENDERER,
1
),
RENDERER_ACCURACY(
"gpu_accuracy",
Settings.SECTION_RENDERER,
0
),
RENDERER_RESOLUTION(
"resolution_setup",
Settings.SECTION_RENDERER,
2
),
RENDERER_VSYNC(
"use_vsync",
Settings.SECTION_RENDERER,
0
),
RENDERER_SCALING_FILTER(
"scaling_filter",
Settings.SECTION_RENDERER,
1
),
RENDERER_ANTI_ALIASING(
"anti_aliasing",
Settings.SECTION_RENDERER,
0
),
RENDERER_SCREEN_LAYOUT(
"screen_layout",
Settings.SECTION_RENDERER,
Settings.LayoutOption_MobileLandscape
),
RENDERER_ASPECT_RATIO(
"aspect_ratio",
Settings.SECTION_RENDERER,
0
),
AUDIO_VOLUME(
"volume",
Settings.SECTION_AUDIO,
100
);
override var int: Int = defaultValue override val int: Int
get() = NativeConfig.getInt(key, false)
override fun setInt(value: Int) = NativeConfig.setInt(key, value)
override val defaultValue: Int by lazy {
androidDefault ?: NativeConfig.getInt(key, true)
}
override val valueAsString: String override val valueAsString: String
get() = int.toString() get() = int.toString()
override val isRuntimeEditable: Boolean override fun reset() = NativeConfig.setInt(key, defaultValue)
get() {
for (setting in NOT_RUNTIME_EDITABLE) {
if (setting == this) {
return false
}
}
return true
}
companion object {
private val NOT_RUNTIME_EDITABLE = listOf(
RENDERER_USE_DISK_SHADER_CACHE,
RENDERER_ASYNCHRONOUS_SHADERS,
RENDERER_DEBUG,
RENDERER_BACKEND,
RENDERER_RESOLUTION,
RENDERER_VSYNC
)
fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key }
fun clear() = IntSetting.values().forEach { it.int = it.defaultValue }
}
} }

View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class LongSetting(
override val key: String,
override val category: Settings.Category
) : AbstractLongSetting {
CUSTOM_RTC("custom_rtc", Settings.Category.System);
override val long: Long
get() = NativeConfig.getLong(key, false)
override fun setLong(value: Long) = NativeConfig.setLong(key, value)
override val defaultValue: Long by lazy { NativeConfig.getLong(key, true) }
override val valueAsString: String
get() = long.toString()
override fun reset() = NativeConfig.setLong(key, defaultValue)
}

View File

@ -1,37 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model
/**
* A semantically-related group of Settings objects. These Settings are
* internally stored as a HashMap.
*/
class SettingSection(val name: String) {
val settings = HashMap<String, AbstractSetting>()
/**
* Convenience method; inserts a value directly into the backing HashMap.
*
* @param setting The Setting to be inserted.
*/
fun putSetting(setting: AbstractSetting) {
settings[setting.key!!] = setting
}
/**
* Convenience method; gets a value directly from the backing HashMap.
*
* @param key Used to retrieve the Setting.
* @return A Setting object (you should probably cast this before using)
*/
fun getSetting(key: String): AbstractSetting? {
return settings[key]
}
fun mergeSection(settingSection: SettingSection) {
for (setting in settingSection.settings.values) {
putSetting(setting)
}
}
}

View File

@ -4,104 +4,74 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import android.text.TextUtils import android.text.TextUtils
import java.util.* import android.widget.Toast
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
class Settings { object Settings {
private var gameId: String? = null private val context get() = YuzuApplication.appContext
var isLoaded = false fun saveSettings(gameId: String = "") {
/**
* A HashMap<String></String>, SettingSection> that constructs a new SettingSection instead of returning null
* when getting a key not already in the map
*/
class SettingsSectionMap : HashMap<String, SettingSection?>() {
override operator fun get(key: String): SettingSection? {
if (!super.containsKey(key)) {
val section = SettingSection(key)
super.put(key, section)
return section
}
return super.get(key)
}
}
var sections: HashMap<String, SettingSection?> = SettingsSectionMap()
fun getSection(sectionName: String): SettingSection? {
return sections[sectionName]
}
val isEmpty: Boolean
get() = sections.isEmpty()
fun loadSettings(view: SettingsActivityView? = null) {
sections = SettingsSectionMap()
loadYuzuSettings(view)
if (!TextUtils.isEmpty(gameId)) {
loadCustomGameSettings(gameId!!, view)
}
isLoaded = true
}
private fun loadYuzuSettings(view: SettingsActivityView?) {
for ((fileName) in configFileSectionsMap) {
sections.putAll(SettingsFile.readFile(fileName, view))
}
}
private fun loadCustomGameSettings(gameId: String, view: SettingsActivityView?) {
// Custom game settings
mergeSections(SettingsFile.readCustomGameSettings(gameId, view))
}
private fun mergeSections(updatedSections: HashMap<String, SettingSection?>) {
for ((key, updatedSection) in updatedSections) {
if (sections.containsKey(key)) {
val originalSection = sections[key]
originalSection!!.mergeSection(updatedSection!!)
} else {
sections[key] = updatedSection
}
}
}
fun loadSettings(gameId: String, view: SettingsActivityView) {
this.gameId = gameId
loadSettings(view)
}
fun saveSettings(view: SettingsActivityView) {
if (TextUtils.isEmpty(gameId)) { if (TextUtils.isEmpty(gameId)) {
view.showToastMessage( Toast.makeText(
YuzuApplication.appContext.getString(R.string.ini_saved), context,
false context.getString(R.string.ini_saved),
) Toast.LENGTH_SHORT
).show()
for ((fileName, sectionNames) in configFileSectionsMap) { SettingsFile.saveFile(SettingsFile.FILE_NAME_CONFIG)
val iniSections = TreeMap<String, SettingSection>()
for (section in sectionNames) {
iniSections[section] = sections[section]!!
}
SettingsFile.saveFile(fileName, iniSections, view)
}
} else { } else {
// Custom game settings // TODO: Save custom game settings
view.showToastMessage( Toast.makeText(
YuzuApplication.appContext.getString(R.string.gameid_saved, gameId), context,
false context.getString(R.string.gameid_saved, gameId),
Toast.LENGTH_SHORT
).show()
}
}
enum class Category {
Android,
Audio,
Core,
Cpu,
CpuDebug,
CpuUnsafe,
Renderer,
RendererAdvanced,
RendererDebug,
System,
SystemAudio,
DataStorage,
Debugging,
DebuggingGraphics,
Miscellaneous,
Network,
WebService,
AddOns,
Controls,
Ui,
UiGeneral,
UiLayout,
UiGameList,
Screenshots,
Shortcuts,
Multiplayer,
Services,
Paths,
MaxEnum
}
val settingsList = listOf<AbstractSetting>(
*BooleanSetting.values(),
*ByteSetting.values(),
*ShortSetting.values(),
*IntSetting.values(),
*FloatSetting.values(),
*LongSetting.values(),
*StringSetting.values()
) )
SettingsFile.saveCustomGameSettings(gameId, sections)
}
}
companion object {
const val SECTION_GENERAL = "General" const val SECTION_GENERAL = "General"
const val SECTION_SYSTEM = "System" const val SECTION_SYSTEM = "System"
const val SECTION_RENDERER = "Renderer" const val SECTION_RENDERER = "Renderer"
@ -154,8 +124,6 @@ class Settings {
const val PREF_THEME_MODE = "ThemeMode" const val PREF_THEME_MODE = "ThemeMode"
const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds" const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
val overlayPreferences = listOf( val overlayPreferences = listOf(
PREF_OVERLAY_VERSION, PREF_OVERLAY_VERSION,
PREF_CONTROL_SCALE, PREF_CONTROL_SCALE,
@ -183,16 +151,4 @@ class Settings {
const val LayoutOption_Unspecified = 0 const val LayoutOption_Unspecified = 0
const val LayoutOption_MobilePortrait = 4 const val LayoutOption_MobilePortrait = 4
const val LayoutOption_MobileLandscape = 5 const val LayoutOption_MobileLandscape = 5
init {
configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
listOf(
SECTION_GENERAL,
SECTION_SYSTEM,
SECTION_RENDERER,
SECTION_AUDIO,
SECTION_CPU
)
}
}
} }

View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class ShortSetting(
override val key: String,
override val category: Settings.Category
) : AbstractShortSetting {
RENDERER_SPEED_LIMIT("speed_limit", Settings.Category.Core);
override val short: Short
get() = NativeConfig.getShort(key, false)
override fun setShort(value: Short) = NativeConfig.setShort(key, value)
override val defaultValue: Short by lazy { NativeConfig.getShort(key, true) }
override val valueAsString: String
get() = short.toString()
override fun reset() = NativeConfig.setShort(key, defaultValue)
}

View File

@ -3,36 +3,24 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class StringSetting( enum class StringSetting(
override val key: String, override val key: String,
override val section: String, override val category: Settings.Category
override val defaultValue: String
) : AbstractStringSetting { ) : AbstractStringSetting {
AUDIO_OUTPUT_ENGINE("output_engine", Settings.SECTION_AUDIO, "auto"), // No string settings currently exist
CUSTOM_RTC("custom_rtc", Settings.SECTION_SYSTEM, "0"); EMPTY_SETTING("", Settings.Category.UiGeneral);
override var string: String = defaultValue override val string: String
get() = NativeConfig.getString(key, false)
override fun setString(value: String) = NativeConfig.setString(key, value)
override val defaultValue: String by lazy { NativeConfig.getString(key, true) }
override val valueAsString: String override val valueAsString: String
get() = string get() = string
override val isRuntimeEditable: Boolean override fun reset() = NativeConfig.setString(key, defaultValue)
get() {
for (setting in NOT_RUNTIME_EDITABLE) {
if (setting == this) {
return false
}
}
return true
}
companion object {
private val NOT_RUNTIME_EDITABLE = listOf(
CUSTOM_RTC
)
fun from(key: String): StringSetting? = StringSetting.values().firstOrNull { it.key == key }
fun clear() = StringSetting.values().forEach { it.string = it.defaultValue }
}
} }

View File

@ -3,29 +3,16 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
class DateTimeSetting( class DateTimeSetting(
setting: AbstractSetting?, private val longSetting: AbstractLongSetting,
titleId: Int, titleId: Int,
descriptionId: Int, descriptionId: Int
val key: String? = null, ) : SettingsItem(longSetting, titleId, descriptionId) {
private val defaultValue: String? = null
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_DATETIME_SETTING override val type = TYPE_DATETIME_SETTING
val value: String var value: Long
get() = if (setting != null) { get() = longSetting.long
val setting = setting as AbstractStringSetting set(value) = (setting as AbstractLongSetting).setLong(value)
setting.string
} else {
defaultValue!!
}
fun setSelectedValue(datetime: String): AbstractStringSetting {
val stringSetting = setting as AbstractStringSetting
stringSetting.string = datetime
return stringSetting
}
} }

View File

@ -5,6 +5,6 @@ package org.yuzu.yuzu_emu.features.settings.model.view
class HeaderSetting( class HeaderSetting(
titleId: Int titleId: Int
) : SettingsItem(null, titleId, 0) { ) : SettingsItem(emptySetting, titleId, 0) {
override val type = TYPE_HEADER override val type = TYPE_HEADER
} }

View File

@ -8,6 +8,6 @@ class RunnableSetting(
descriptionId: Int, descriptionId: Int,
val isRuntimeRunnable: Boolean, val isRuntimeRunnable: Boolean,
val runnable: () -> Unit val runnable: () -> Unit
) : SettingsItem(null, titleId, descriptionId) { ) : SettingsItem(emptySetting, titleId, descriptionId) {
override val type = TYPE_RUNNABLE override val type = TYPE_RUNNABLE
} }

View File

@ -4,7 +4,15 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.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.ShortSetting
/** /**
* ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
@ -14,7 +22,7 @@ import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
* file.) * file.)
*/ */
abstract class SettingsItem( abstract class SettingsItem(
var setting: AbstractSetting?, val setting: AbstractSetting,
val nameId: Int, val nameId: Int,
val descriptionId: Int val descriptionId: Int
) { ) {
@ -23,7 +31,7 @@ abstract class SettingsItem(
val isEditable: Boolean val isEditable: Boolean
get() { get() {
if (!NativeLibrary.isRunning()) return true if (!NativeLibrary.isRunning()) return true
return setting?.isRuntimeEditable ?: false return setting.isRuntimeModifiable
} }
companion object { companion object {
@ -35,5 +43,240 @@ abstract class SettingsItem(
const val TYPE_STRING_SINGLE_CHOICE = 5 const val TYPE_STRING_SINGLE_CHOICE = 5
const val TYPE_DATETIME_SETTING = 6 const val TYPE_DATETIME_SETTING = 6
const val TYPE_RUNNABLE = 7 const val TYPE_RUNNABLE = 7
const val FASTMEM_COMBINED = "fastmem_combined"
val emptySetting = object : AbstractSetting {
override val key: String = ""
override val category: Settings.Category = Settings.Category.Ui
override val defaultValue: Any = false
override fun reset() {}
}
// Extension for putting SettingsItems into a hashmap without repeating yourself
fun HashMap<String, SettingsItem>.put(item: SettingsItem) {
put(item.setting.key, item)
}
// List of all general
val settingsItems = HashMap<String, SettingsItem>().apply {
put(
SwitchSetting(
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
R.string.frame_limit_enable,
R.string.frame_limit_enable_description
)
)
put(
SliderSetting(
ShortSetting.RENDERER_SPEED_LIMIT,
R.string.frame_limit_slider,
R.string.frame_limit_slider_description,
1,
200,
"%"
)
)
put(
SingleChoiceSetting(
IntSetting.CPU_ACCURACY,
R.string.cpu_accuracy,
0,
R.array.cpuAccuracyNames,
R.array.cpuAccuracyValues
)
)
put(
SwitchSetting(
BooleanSetting.PICTURE_IN_PICTURE,
R.string.picture_in_picture,
R.string.picture_in_picture_description
)
)
put(
SwitchSetting(
BooleanSetting.USE_DOCKED_MODE,
R.string.use_docked_mode,
R.string.use_docked_mode_description
)
)
put(
SingleChoiceSetting(
IntSetting.REGION_INDEX,
R.string.emulated_region,
0,
R.array.regionNames,
R.array.regionValues
)
)
put(
SingleChoiceSetting(
IntSetting.LANGUAGE_INDEX,
R.string.emulated_language,
0,
R.array.languageNames,
R.array.languageValues
)
)
put(
SwitchSetting(
BooleanSetting.USE_CUSTOM_RTC,
R.string.use_custom_rtc,
R.string.use_custom_rtc_description
)
)
put(DateTimeSetting(LongSetting.CUSTOM_RTC, R.string.set_custom_rtc, 0))
put(
SingleChoiceSetting(
IntSetting.RENDERER_ACCURACY,
R.string.renderer_accuracy,
0,
R.array.rendererAccuracyNames,
R.array.rendererAccuracyValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_RESOLUTION,
R.string.renderer_resolution,
0,
R.array.rendererResolutionNames,
R.array.rendererResolutionValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_VSYNC,
R.string.renderer_vsync,
0,
R.array.rendererVSyncNames,
R.array.rendererVSyncValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_SCALING_FILTER,
R.string.renderer_scaling_filter,
0,
R.array.rendererScalingFilterNames,
R.array.rendererScalingFilterValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_ANTI_ALIASING,
R.string.renderer_anti_aliasing,
0,
R.array.rendererAntiAliasingNames,
R.array.rendererAntiAliasingValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_SCREEN_LAYOUT,
R.string.renderer_screen_layout,
0,
R.array.rendererScreenLayoutNames,
R.array.rendererScreenLayoutValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_ASPECT_RATIO,
R.string.renderer_aspect_ratio,
0,
R.array.rendererAspectRatioNames,
R.array.rendererAspectRatioValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE,
R.string.use_disk_shader_cache,
R.string.use_disk_shader_cache_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_FORCE_MAX_CLOCK,
R.string.renderer_force_max_clock,
R.string.renderer_force_max_clock_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS,
R.string.renderer_asynchronous_shaders,
R.string.renderer_asynchronous_shaders_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_REACTIVE_FLUSHING,
R.string.renderer_reactive_flushing,
R.string.renderer_reactive_flushing_description
)
)
put(
SingleChoiceSetting(
IntSetting.AUDIO_OUTPUT_ENGINE,
R.string.audio_output_engine,
0,
R.array.outputEngineEntries,
R.array.outputEngineValues
)
)
put(
SliderSetting(
ByteSetting.AUDIO_VOLUME,
R.string.audio_volume,
R.string.audio_volume_description,
0,
100,
"%"
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_BACKEND,
R.string.renderer_api,
0,
R.array.rendererApiNames,
R.array.rendererApiValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_DEBUG,
R.string.renderer_debug,
R.string.renderer_debug_description
)
)
put(
SwitchSetting(
BooleanSetting.CPU_DEBUG_MODE,
R.string.cpu_debug_mode,
R.string.cpu_debug_mode_description
)
)
val fastmem = object : AbstractBooleanSetting {
override val boolean: Boolean
get() =
BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean
override fun setBoolean(value: Boolean) {
BooleanSetting.FASTMEM.setBoolean(value)
BooleanSetting.FASTMEM_EXCLUSIVES.setBoolean(value)
}
override val key: String = FASTMEM_COMBINED
override val category = Settings.Category.Cpu
override val isRuntimeModifiable: Boolean = false
override val defaultValue: Boolean = true
override fun reset() = setBoolean(defaultValue)
}
put(SwitchSetting(fastmem, R.string.fastmem, 0))
}
} }
} }

View File

@ -4,36 +4,27 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
class SingleChoiceSetting( class SingleChoiceSetting(
setting: AbstractIntSetting?, setting: AbstractSetting,
titleId: Int, titleId: Int,
descriptionId: Int, descriptionId: Int,
val choicesId: Int, val choicesId: Int,
val valuesId: Int, val valuesId: Int
val key: String? = null,
val defaultValue: Int? = null
) : SettingsItem(setting, titleId, descriptionId) { ) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SINGLE_CHOICE override val type = TYPE_SINGLE_CHOICE
val selectedValue: Int var selectedValue: Int
get() = if (setting != null) { get() {
val setting = setting as AbstractIntSetting return when (setting) {
setting.int is AbstractIntSetting -> setting.int
} else { else -> -1
defaultValue!! }
} }
set(value) {
/** when (setting) {
* Write a value to the backing int. If that int was previously null, is AbstractIntSetting -> setting.setInt(value)
* initializes a new one and returns it, so it can be added to the Hashmap. }
*
* @param selection New value of the int.
* @return the existing setting with the new value applied.
*/
fun setSelectedValue(selection: Int): AbstractIntSetting {
val intSetting = setting as AbstractIntSetting
intSetting.int = selection
return intSetting
} }
} }

View File

@ -3,60 +3,39 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import kotlin.math.roundToInt import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.features.settings.model.AbstractShortSetting
import kotlin.math.roundToInt
class SliderSetting( class SliderSetting(
setting: AbstractSetting?, setting: AbstractSetting,
titleId: Int, titleId: Int,
descriptionId: Int, descriptionId: Int,
val min: Int, val min: Int,
val max: Int, val max: Int,
val units: String, val units: String
val key: String? = null,
val defaultValue: Int? = null
) : SettingsItem(setting, titleId, descriptionId) { ) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SLIDER override val type = TYPE_SLIDER
val selectedValue: Int var selectedValue: Int
get() { get() {
val setting = setting ?: return defaultValue!!
return when (setting) { return when (setting) {
is AbstractByteSetting -> setting.byte.toInt()
is AbstractShortSetting -> setting.short.toInt()
is AbstractIntSetting -> setting.int is AbstractIntSetting -> setting.int
is AbstractFloatSetting -> setting.float.roundToInt() is AbstractFloatSetting -> setting.float.roundToInt()
else -> { else -> -1
Log.error("[SliderSetting] Error casting setting type.") }
-1 }
set(value) {
when (setting) {
is AbstractByteSetting -> setting.setByte(value.toByte())
is AbstractShortSetting -> setting.setShort(value.toShort())
is AbstractIntSetting -> setting.setInt(value)
is AbstractFloatSetting -> setting.setFloat(value.toFloat())
} }
} }
} }
/**
* Write a value to the backing int. If that int was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param selection New value of the int.
* @return the existing setting with the new value applied.
*/
fun setSelectedValue(selection: Int): AbstractIntSetting {
val intSetting = setting as AbstractIntSetting
intSetting.int = selection
return intSetting
}
/**
* Write a value to the backing float. If that float was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param selection New value of the float.
* @return the existing setting with the new value applied.
*/
fun setSelectedValue(selection: Float): AbstractFloatSetting {
val floatSetting = setting as AbstractFloatSetting
floatSetting.float = selection
return floatSetting
}
}

View File

@ -3,57 +3,31 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
class StringSingleChoiceSetting( class StringSingleChoiceSetting(
setting: AbstractSetting?, private val stringSetting: AbstractStringSetting,
titleId: Int, titleId: Int,
descriptionId: Int, descriptionId: Int,
val choices: Array<String>, val choices: Array<String>,
val values: Array<String>?, val values: Array<String>
val key: String? = null, ) : SettingsItem(stringSetting, titleId, descriptionId) {
private val defaultValue: String? = null
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_STRING_SINGLE_CHOICE override val type = TYPE_STRING_SINGLE_CHOICE
fun getValueAt(index: Int): String? { fun getValueAt(index: Int): String =
if (values == null) return null if (index >= 0 && index < values.size) values[index] else ""
return if (index >= 0 && index < values.size) {
values[index] var selectedValue: String
} else { get() = stringSetting.string
"" set(value) = stringSetting.setString(value)
}
}
val selectedValue: String
get() = if (setting != null) {
val setting = setting as AbstractStringSetting
setting.string
} else {
defaultValue!!
}
val selectValueIndex: Int val selectValueIndex: Int
get() { get() {
val selectedValue = selectedValue for (i in values.indices) {
for (i in values!!.indices) {
if (values[i] == selectedValue) { if (values[i] == selectedValue) {
return i return i
} }
} }
return -1 return -1
} }
/**
* Write a value to the backing int. If that int was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param selection New value of the int.
* @return the existing setting with the new value applied.
*/
fun setSelectedValue(selection: String): AbstractStringSetting {
val stringSetting = setting as AbstractStringSetting
stringSetting.string = selection
return stringSetting
}
} }

View File

@ -7,6 +7,6 @@ class SubmenuSetting(
titleId: Int, titleId: Int,
descriptionId: Int, descriptionId: Int,
val menuKey: String val menuKey: String
) : SettingsItem(null, titleId, descriptionId) { ) : SettingsItem(emptySetting, titleId, descriptionId) {
override val type = TYPE_SUBMENU override val type = TYPE_SUBMENU
} }

View File

@ -10,53 +10,22 @@ import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
class SwitchSetting( class SwitchSetting(
setting: AbstractSetting, setting: AbstractSetting,
titleId: Int, titleId: Int,
descriptionId: Int, descriptionId: Int
val key: String? = null,
val defaultValue: Any? = null
) : SettingsItem(setting, titleId, descriptionId) { ) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SWITCH override val type = TYPE_SWITCH
val isChecked: Boolean var checked: Boolean
get() { get() {
if (setting == null) { return when (setting) {
return defaultValue as Boolean is AbstractIntSetting -> setting.int == 1
} is AbstractBooleanSetting -> setting.boolean
else -> false
// Try integer setting }
try { }
val setting = setting as AbstractIntSetting set(value) {
return setting.int == 1 when (setting) {
} catch (_: ClassCastException) { is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
} is AbstractBooleanSetting -> setting.setBoolean(value)
}
// Try boolean setting
try {
val setting = setting as AbstractBooleanSetting
return setting.boolean
} catch (_: ClassCastException) {
}
return defaultValue as Boolean
}
/**
* Write a value to the backing boolean. If that boolean was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param checked Pretty self explanatory.
* @return the existing setting with the new value applied.
*/
fun setChecked(checked: Boolean): AbstractSetting {
// Try integer setting
try {
val setting = setting as AbstractIntSetting
setting.int = if (checked) 1 else 0
return setting
} catch (_: ClassCastException) {
}
// Try boolean setting
val setting = setting as AbstractBooleanSetting
setting.boolean = checked
return setting
} }
} }

View File

@ -3,42 +3,34 @@
package org.yuzu.yuzu_emu.features.settings.ui package org.yuzu.yuzu_emu.features.settings.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.View import android.view.View
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navArgs
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import java.io.IOException import java.io.IOException
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.FloatSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
import org.yuzu.yuzu_emu.model.SettingsViewModel
import org.yuzu.yuzu_emu.utils.* import org.yuzu.yuzu_emu.utils.*
class SettingsActivity : AppCompatActivity(), SettingsActivityView { class SettingsActivity : AppCompatActivity() {
private val presenter = SettingsActivityPresenter(this)
private lateinit var binding: ActivitySettingsBinding private lateinit var binding: ActivitySettingsBinding
private val settingsViewModel: SettingsViewModel by viewModels() private val args by navArgs<SettingsActivityArgs>()
override val settings: Settings get() = settingsViewModel.settings private val settingsViewModel: SettingsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.setTheme(this) ThemeHelper.setTheme(this)
@ -48,16 +40,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
binding = ActivitySettingsBinding.inflate(layoutInflater) binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
settingsViewModel.game = args.game
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
navHostFragment.navController.setGraph(R.navigation.settings_navigation, intent.extras)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
val launcher = intent if (savedInstanceState != null) {
val gameID = launcher.getStringExtra(ARG_GAME_ID) settingsViewModel.shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
val menuTag = launcher.getStringExtra(ARG_MENU_TAG) }
presenter.onCreate(savedInstanceState, menuTag!!, gameID!!)
// Show "Back" button in the action bar for navigation
setSupportActionBar(binding.toolbarSettings)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (InsetsHelper.getSystemGestureType(applicationContext) != if (InsetsHelper.getSystemGestureType(applicationContext) !=
InsetsHelper.GESTURE_NAVIGATION InsetsHelper.GESTURE_NAVIGATION
@ -73,6 +66,28 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
) )
} }
settingsViewModel.shouldRecreate.observe(this) {
if (it) {
settingsViewModel.setShouldRecreate(false)
recreate()
}
}
settingsViewModel.shouldNavigateBack.observe(this) {
if (it) {
settingsViewModel.setShouldNavigateBack(false)
navigateBack()
}
}
settingsViewModel.shouldShowResetSettingsDialog.observe(this) {
if (it) {
settingsViewModel.setShouldShowResetSettingsDialog(false)
ResetSettingsDialogFragment().show(
supportFragmentManager,
ResetSettingsDialogFragment.TAG
)
}
}
onBackPressedDispatcher.addCallback( onBackPressedDispatcher.addCallback(
this, this,
object : OnBackPressedCallback(true) { object : OnBackPressedCallback(true) {
@ -83,34 +98,28 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
setInsets() setInsets()
} }
override fun onSupportNavigateUp(): Boolean { fun navigateBack() {
navigateBack() val navHostFragment =
return true supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
} if (navHostFragment.childFragmentManager.backStackEntryCount > 0) {
navHostFragment.navController.popBackStack()
private fun navigateBack() {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
} else { } else {
finish() finish()
} }
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.menu_settings, menu)
return true
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
// Critical: If super method is not called, rotations will be busted. // Critical: If super method is not called, rotations will be busted.
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
presenter.saveState(outState) outState.putBoolean(KEY_SHOULD_SAVE, settingsViewModel.shouldSave)
} }
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
presenter.onStart() // TODO: Load custom settings contextually
if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start()
}
} }
/** /**
@ -120,143 +129,51 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
*/ */
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
presenter.onStop(isFinishing) if (isFinishing && settingsViewModel.shouldSave) {
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
Settings.saveSettings()
}
} }
override fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String) { override fun onDestroy() {
if (!addToStack && settingsFragment != null) { settingsViewModel.clear()
return super.onDestroy()
}
val transaction = supportFragmentManager.beginTransaction()
if (addToStack) {
if (areSystemAnimationsEnabled()) {
transaction.setCustomAnimations(
R.anim.anim_settings_fragment_in,
R.anim.anim_settings_fragment_out,
0,
R.anim.anim_pop_settings_fragment_out
)
}
transaction.addToBackStack(null)
}
transaction.replace(
R.id.frame_content,
SettingsFragment.newInstance(menuTag, gameId),
FRAGMENT_TAG
)
transaction.commit()
}
private fun areSystemAnimationsEnabled(): Boolean {
val duration = android.provider.Settings.Global.getFloat(
contentResolver,
android.provider.Settings.Global.ANIMATOR_DURATION_SCALE,
1f
)
val transition = android.provider.Settings.Global.getFloat(
contentResolver,
android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE,
1f
)
return duration != 0f && transition != 0f
}
override fun onSettingsFileLoaded() {
val fragment: SettingsFragmentView? = settingsFragment
fragment?.loadSettingsList()
}
override fun onSettingsFileNotFound() {
val fragment: SettingsFragmentView? = settingsFragment
fragment?.loadSettingsList()
}
override fun showToastMessage(message: String, is_long: Boolean) {
Toast.makeText(
this,
message,
if (is_long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
).show()
}
override fun onSettingChanged() {
presenter.onSettingChanged()
} }
fun onSettingsReset() { fun onSettingsReset() {
// Prevents saving to a non-existent settings file // Prevents saving to a non-existent settings file
presenter.onSettingsReset() settingsViewModel.shouldSave = false
// Reset the static memory representation of each setting
BooleanSetting.clear()
FloatSetting.clear()
IntSetting.clear()
StringSetting.clear()
// Delete settings file because the user may have changed values that do not exist in the UI // Delete settings file because the user may have changed values that do not exist in the UI
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
if (!settingsFile.delete()) { if (!settingsFile.delete()) {
throw IOException("Failed to delete $settingsFile") throw IOException("Failed to delete $settingsFile")
} }
Settings.settingsList.forEach { it.reset() }
showToastMessage(getString(R.string.settings_reset), true) Toast.makeText(
applicationContext,
getString(R.string.settings_reset),
Toast.LENGTH_LONG
).show()
finish() finish()
} }
fun setToolbarTitle(title: String) {
binding.toolbarSettingsLayout.title = title
}
private val settingsFragment: SettingsFragment?
get() = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as SettingsFragment?
private fun setInsets() { private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener( ViewCompat.setOnApplyWindowInsetsListener(
binding.frameContent binding.navigationBarShade
) { view: View, windowInsets: WindowInsetsCompat -> ) { view: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
view.updatePadding(
left = barInsets.left + cutoutInsets.left,
right = barInsets.right + cutoutInsets.right
)
val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams val mlpShade = view.layoutParams as MarginLayoutParams
mlpAppBar.leftMargin = barInsets.left + cutoutInsets.left
mlpAppBar.rightMargin = barInsets.right + cutoutInsets.right
binding.appbarSettings.layoutParams = mlpAppBar
val mlpShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
mlpShade.height = barInsets.bottom mlpShade.height = barInsets.bottom
binding.navigationBarShade.layoutParams = mlpShade view.layoutParams = mlpShade
windowInsets windowInsets
} }
} }
companion object { companion object {
private const val ARG_MENU_TAG = "menu_tag" private const val KEY_SHOULD_SAVE = "should_save"
private const val ARG_GAME_ID = "game_id"
private const val FRAGMENT_TAG = "settings"
fun launch(context: Context, menuTag: String?, gameId: String?) {
val settings = Intent(context, SettingsActivity::class.java)
settings.putExtra(ARG_MENU_TAG, menuTag)
settings.putExtra(ARG_GAME_ID, gameId)
context.startActivity(settings)
}
fun launch(
context: Context,
launcher: ActivityResultLauncher<Intent>,
menuTag: String?,
gameId: String?
) {
val settings = Intent(context, SettingsActivity::class.java)
settings.putExtra(ARG_MENU_TAG, menuTag)
settings.putExtra(ARG_GAME_ID, gameId)
launcher.launch(settings)
}
} }
} }

View File

@ -1,90 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.content.Context
import android.os.Bundle
import android.text.TextUtils
import java.io.File
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.Log
class SettingsActivityPresenter(private val activityView: SettingsActivityView) {
val settings: Settings get() = activityView.settings
private var shouldSave = false
private lateinit var menuTag: String
private lateinit var gameId: String
fun onCreate(savedInstanceState: Bundle?, menuTag: String, gameId: String) {
this.menuTag = menuTag
this.gameId = gameId
if (savedInstanceState != null) {
shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
}
}
fun onStart() {
prepareDirectoriesIfNeeded()
}
private fun loadSettingsUI() {
if (!settings.isLoaded) {
if (!TextUtils.isEmpty(gameId)) {
settings.loadSettings(gameId, activityView)
} else {
settings.loadSettings(activityView)
}
}
activityView.showSettingsFragment(menuTag, false, gameId)
activityView.onSettingsFileLoaded()
}
private fun prepareDirectoriesIfNeeded() {
val configFile =
File(
"${DirectoryInitialization.userDirectory}/config/" +
"${SettingsFile.FILE_NAME_CONFIG}.ini"
)
if (!configFile.exists()) {
Log.error(
"${DirectoryInitialization.userDirectory}/config/" +
"${SettingsFile.FILE_NAME_CONFIG}.ini"
)
Log.error("yuzu config file could not be found!")
}
if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start(activityView as Context)
}
loadSettingsUI()
}
fun onStop(finishing: Boolean) {
if (finishing && shouldSave) {
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
settings.saveSettings(activityView)
}
NativeLibrary.reloadSettings()
}
fun onSettingChanged() {
shouldSave = true
}
fun onSettingsReset() {
shouldSave = false
}
fun saveState(outState: Bundle) {
outState.putBoolean(KEY_SHOULD_SAVE, shouldSave)
}
companion object {
private const val KEY_SHOULD_SAVE = "should_save"
}
}

View File

@ -1,57 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import org.yuzu.yuzu_emu.features.settings.model.Settings
/**
* Abstraction for the Activity that manages SettingsFragments.
*/
interface SettingsActivityView {
/**
* Show a new SettingsFragment.
*
* @param menuTag Identifier for the settings group that should be displayed.
* @param addToStack Whether or not this fragment should replace a previous one.
*/
fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String)
/**
* Called by a contained Fragment to get access to the Setting HashMap
* loaded from disk, so that each Fragment doesn't need to perform its own
* read operation.
*
* @return A HashMap of Settings.
*/
val settings: Settings
/**
* Called when a load operation completes.
*/
fun onSettingsFileLoaded()
/**
* Called when a load operation fails.
*/
fun onSettingsFileNotFound()
/**
* Display a popup text message on screen.
*
* @param message The contents of the onscreen message.
* @param is_long Whether this should be a long Toast or short one.
*/
fun showToastMessage(message: String, is_long: Boolean)
/**
* End the activity.
*/
fun finish()
/**
* Called by a containing Fragment to tell the Activity that a setting was changed;
* unless this has been called, the Activity will not save to disk.
*/
fun onSettingChanged()
}

View File

@ -4,51 +4,54 @@
package org.yuzu.yuzu_emu.features.settings.ui package org.yuzu.yuzu_emu.features.settings.ui
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.icu.util.Calendar import android.icu.util.Calendar
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.text.format.DateFormat import android.text.format.DateFormat
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import androidx.fragment.app.Fragment
import androidx.appcompat.app.AlertDialog import androidx.lifecycle.Lifecycle
import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider
import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat import com.google.android.material.timepicker.TimeFormat
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogSliderBinding import org.yuzu.yuzu_emu.SettingsNavigationDirections
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
import org.yuzu.yuzu_emu.features.settings.model.FloatSetting
import org.yuzu.yuzu_emu.features.settings.model.view.* import org.yuzu.yuzu_emu.features.settings.model.view.*
import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* import org.yuzu.yuzu_emu.features.settings.ui.viewholder.*
import org.yuzu.yuzu_emu.fragments.SettingsDialogFragment
import org.yuzu.yuzu_emu.model.SettingsViewModel
class SettingsAdapter( class SettingsAdapter(
private val fragmentView: SettingsFragmentView, private val fragment: Fragment,
private val context: Context private val context: Context
) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener { ) : ListAdapter<SettingsItem, SettingViewHolder>(
private var settings: ArrayList<SettingsItem>? = null AsyncDifferConfig.Builder(DiffCallback()).build()
private var clickedItem: SettingsItem? = null ) {
private var clickedPosition: Int private val settingsViewModel: SettingsViewModel
private var dialog: AlertDialog? = null get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java]
private var sliderProgress = 0
private var textSliderValue: TextView? = null
private var defaultCancelListener =
DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
init { init {
clickedPosition = -1 fragment.viewLifecycleOwner.lifecycleScope.launch {
fragment.repeatOnLifecycle(Lifecycle.State.STARTED) {
settingsViewModel.adapterItemChanged.collect {
if (it != -1) {
notifyItemChanged(it)
settingsViewModel.setAdapterItemChanged(-1)
}
}
}
}
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
@ -90,67 +93,41 @@ class SettingsAdapter(
} }
override fun onBindViewHolder(holder: SettingViewHolder, position: Int) { override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
holder.bind(getItem(position)) holder.bind(currentList[position])
} }
private fun getItem(position: Int): SettingsItem { override fun getItemCount(): Int = currentList.size
return settings!![position]
}
override fun getItemCount(): Int {
return if (settings != null) {
settings!!.size
} else {
0
}
}
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return getItem(position).type return currentList[position].type
} }
fun setSettingsList(settings: ArrayList<SettingsItem>?) { fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
this.settings = settings item.checked = checked
notifyDataSetChanged() settingsViewModel.setShouldReloadSettingsList(true)
} settingsViewModel.shouldSave = true
fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) {
val setting = item.setChecked(checked)
fragmentView.putSetting(setting)
fragmentView.onSettingChanged()
}
private fun onSingleChoiceClick(item: SingleChoiceSetting) {
clickedItem = item
val value = getSelectionForSingleChoiceValue(item)
dialog = MaterialAlertDialogBuilder(context)
.setTitle(item.nameId)
.setSingleChoiceItems(item.choicesId, value, this)
.show()
} }
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) { fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
clickedPosition = position SettingsDialogFragment.newInstance(
onSingleChoiceClick(item) settingsViewModel,
} item,
SettingsItem.TYPE_SINGLE_CHOICE,
private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) { position
clickedItem = item ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
dialog = MaterialAlertDialogBuilder(context)
.setTitle(item.nameId)
.setSingleChoiceItems(item.choices, item.selectValueIndex, this)
.show()
} }
fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) { fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
clickedPosition = position SettingsDialogFragment.newInstance(
onStringSingleChoiceClick(item) settingsViewModel,
item,
SettingsItem.TYPE_STRING_SINGLE_CHOICE,
position
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
} }
fun onDateTimeClick(item: DateTimeSetting, position: Int) { fun onDateTimeClick(item: DateTimeSetting, position: Int) {
clickedItem = item val storedTime = item.value * 1000
clickedPosition = position
val storedTime = java.lang.Long.decode(item.value) * 1000
// Helper to extract hour and minute from epoch time // Helper to extract hour and minute from epoch time
val calendar: Calendar = Calendar.getInstance() val calendar: Calendar = Calendar.getInstance()
@ -158,7 +135,7 @@ class SettingsAdapter(
calendar.timeZone = TimeZone.getTimeZone("UTC") calendar.timeZone = TimeZone.getTimeZone("UTC")
var timeFormat: Int = TimeFormat.CLOCK_12H var timeFormat: Int = TimeFormat.CLOCK_12H
if (DateFormat.is24HourFormat(fragmentView.activityView as AppCompatActivity)) { if (DateFormat.is24HourFormat(context)) {
timeFormat = TimeFormat.CLOCK_24H timeFormat = TimeFormat.CLOCK_24H
} }
@ -175,7 +152,7 @@ class SettingsAdapter(
datePicker.addOnPositiveButtonClickListener { datePicker.addOnPositiveButtonClickListener {
timePicker.show( timePicker.show(
(fragmentView.activityView as AppCompatActivity).supportFragmentManager, fragment.childFragmentManager,
"TimePicker" "TimePicker"
) )
} }
@ -183,157 +160,50 @@ class SettingsAdapter(
var epochTime: Long = datePicker.selection!! / 1000 var epochTime: Long = datePicker.selection!! / 1000
epochTime += timePicker.hour.toLong() * 60 * 60 epochTime += timePicker.hour.toLong() * 60 * 60
epochTime += timePicker.minute.toLong() * 60 epochTime += timePicker.minute.toLong() * 60
val rtcString = epochTime.toString() if (item.value != epochTime) {
if (item.value != rtcString) { settingsViewModel.shouldSave = true
fragmentView.onSettingChanged() notifyItemChanged(position)
item.value = epochTime
} }
notifyItemChanged(clickedPosition)
val setting = item.setSelectedValue(rtcString)
fragmentView.putSetting(setting)
clickedItem = null
} }
datePicker.show( datePicker.show(
(fragmentView.activityView as AppCompatActivity).supportFragmentManager, fragment.childFragmentManager,
"DatePicker" "DatePicker"
) )
} }
fun onSliderClick(item: SliderSetting, position: Int) { fun onSliderClick(item: SliderSetting, position: Int) {
clickedItem = item SettingsDialogFragment.newInstance(
clickedPosition = position settingsViewModel,
sliderProgress = item.selectedValue item,
SettingsItem.TYPE_SLIDER,
val inflater = LayoutInflater.from(context) position
val sliderBinding = DialogSliderBinding.inflate(inflater) ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
textSliderValue = sliderBinding.textValue
textSliderValue!!.text = sliderProgress.toString()
sliderBinding.textUnits.text = item.units
sliderBinding.slider.apply {
valueFrom = item.min.toFloat()
valueTo = item.max.toFloat()
value = sliderProgress.toFloat()
addOnChangeListener { _: Slider, value: Float, _: Boolean ->
sliderProgress = value.toInt()
textSliderValue!!.text = sliderProgress.toString()
}
}
dialog = MaterialAlertDialogBuilder(context)
.setTitle(item.nameId)
.setView(sliderBinding.root)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
.setNeutralButton(R.string.slider_default) { dialog: DialogInterface, which: Int ->
sliderBinding.slider.value = item.defaultValue!!.toFloat()
onClick(dialog, which)
}
.show()
} }
fun onSubmenuClick(item: SubmenuSetting) { fun onSubmenuClick(item: SubmenuSetting) {
fragmentView.loadSubMenu(item.menuKey) val action = SettingsNavigationDirections.actionGlobalSettingsFragment(item.menuKey, null)
fragment.view?.findNavController()?.navigate(action)
} }
override fun onClick(dialog: DialogInterface, which: Int) { fun onLongClick(item: SettingsItem, position: Int): Boolean {
when (clickedItem) { SettingsDialogFragment.newInstance(
is SingleChoiceSetting -> { settingsViewModel,
val scSetting = clickedItem as SingleChoiceSetting item,
val value = getValueForSingleChoiceSelection(scSetting, which) SettingsDialogFragment.TYPE_RESET_SETTING,
if (scSetting.selectedValue != value) { position
fragmentView.onSettingChanged() ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
}
// Get the backing Setting, which may be null (if for example it was missing from the file)
val setting = scSetting.setSelectedValue(value)
fragmentView.putSetting(setting)
closeDialog()
}
is StringSingleChoiceSetting -> {
val scSetting = clickedItem as StringSingleChoiceSetting
val value = scSetting.getValueAt(which)
if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
val setting = scSetting.setSelectedValue(value!!)
fragmentView.putSetting(setting)
closeDialog()
}
is SliderSetting -> {
val sliderSetting = clickedItem as SliderSetting
if (sliderSetting.selectedValue != sliderProgress) {
fragmentView.onSettingChanged()
}
if (sliderSetting.setting is FloatSetting) {
val value = sliderProgress.toFloat()
val setting = sliderSetting.setSelectedValue(value)
fragmentView.putSetting(setting)
} else {
val setting = sliderSetting.setSelectedValue(sliderProgress)
fragmentView.putSetting(setting)
}
closeDialog()
}
}
clickedItem = null
sliderProgress = -1
}
fun onLongClick(setting: AbstractSetting, position: Int): Boolean {
MaterialAlertDialogBuilder(context)
.setMessage(R.string.reset_setting_confirmation)
.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, which: Int ->
when (setting) {
is AbstractBooleanSetting -> setting.boolean = setting.defaultValue as Boolean
is AbstractFloatSetting -> setting.float = setting.defaultValue as Float
is AbstractIntSetting -> setting.int = setting.defaultValue as Int
is AbstractStringSetting -> setting.string = setting.defaultValue as String
}
notifyItemChanged(position)
fragmentView.onSettingChanged()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
return true return true
} }
fun closeDialog() { private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
if (dialog != null) { override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
if (clickedPosition != -1) { return oldItem.setting.key == newItem.setting.key
notifyItemChanged(clickedPosition)
clickedPosition = -1
}
dialog!!.dismiss()
dialog = null
}
} }
private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int { override fun areContentsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
val valuesId = item.valuesId return oldItem.setting.key == newItem.setting.key
return if (valuesId > 0) {
val valuesArray = context.resources.getIntArray(valuesId)
valuesArray[which]
} else {
which
} }
} }
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
val value = item.selectedValue
val valuesId = item.valuesId
if (valuesId > 0) {
val valuesArray = context.resources.getIntArray(valuesId)
for (index in valuesArray.indices) {
val current = valuesArray[index]
if (current == value) {
return index
}
}
} else {
return value
}
return -1
}
} }

View File

@ -3,40 +3,43 @@
package org.yuzu.yuzu_emu.features.settings.ui package org.yuzu.yuzu_emu.features.settings.ui
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.divider.MaterialDividerItemDecoration
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.model.SettingsViewModel
class SettingsFragment : Fragment(), SettingsFragmentView { class SettingsFragment : Fragment() {
override var activityView: SettingsActivityView? = null private lateinit var presenter: SettingsFragmentPresenter
private val fragmentPresenter = SettingsFragmentPresenter(this)
private var settingsAdapter: SettingsAdapter? = null private var settingsAdapter: SettingsAdapter? = null
private var _binding: FragmentSettingsBinding? = null private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun onAttach(context: Context) { private val args by navArgs<SettingsFragmentArgs>()
super.onAttach(context)
activityView = requireActivity() as SettingsActivityView private val settingsViewModel: SettingsViewModel by activityViewModels()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val menuTag = requireArguments().getString(ARGUMENT_MENU_TAG) enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
val gameId = requireArguments().getString(ARGUMENT_GAME_ID) returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
fragmentPresenter.onCreate(menuTag!!, gameId!!) reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
} }
override fun onCreateView( override fun onCreateView(
@ -49,7 +52,14 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
settingsAdapter = SettingsAdapter(this, requireActivity()) settingsAdapter = SettingsAdapter(this, requireContext())
presenter = SettingsFragmentPresenter(
settingsViewModel,
settingsAdapter!!,
args.menuTag,
args.game?.gameId ?: ""
)
val dividerDecoration = MaterialDividerItemDecoration( val dividerDecoration = MaterialDividerItemDecoration(
requireContext(), requireContext(),
LinearLayoutManager.VERTICAL LinearLayoutManager.VERTICAL
@ -57,71 +67,86 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
dividerDecoration.isLastItemDecorated = false dividerDecoration.isLastItemDecorated = false
binding.listSettings.apply { binding.listSettings.apply {
adapter = settingsAdapter adapter = settingsAdapter
layoutManager = LinearLayoutManager(activity) layoutManager = LinearLayoutManager(requireContext())
addItemDecoration(dividerDecoration) addItemDecoration(dividerDecoration)
} }
fragmentPresenter.onViewCreated()
binding.toolbarSettings.setNavigationOnClickListener {
settingsViewModel.setShouldNavigateBack(true)
}
settingsViewModel.toolbarTitle.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) binding.toolbarSettingsLayout.title = it
}
settingsViewModel.shouldReloadSettingsList.observe(viewLifecycleOwner) {
if (it) {
settingsViewModel.setShouldReloadSettingsList(false)
presenter.loadSettingsList()
}
}
settingsViewModel.isUsingSearch.observe(viewLifecycleOwner) {
if (it) {
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
} else {
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
}
}
if (args.menuTag == SettingsFile.FILE_NAME_CONFIG) {
binding.toolbarSettings.inflateMenu(R.menu.menu_settings)
binding.toolbarSettings.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_search -> {
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
view.findNavController()
.navigate(R.id.action_settingsFragment_to_settingsSearchFragment)
true
}
else -> false
}
}
}
presenter.onViewCreated()
setInsets() setInsets()
} }
override fun onDetach() { override fun onResume() {
super.onDetach() super.onResume()
activityView = null settingsViewModel.setIsUsingSearch(false)
if (settingsAdapter != null) {
settingsAdapter!!.closeDialog()
}
}
override fun showSettingsList(settingsList: ArrayList<SettingsItem>) {
settingsAdapter!!.setSettingsList(settingsList)
}
override fun loadSettingsList() {
fragmentPresenter.loadSettingsList()
}
override fun loadSubMenu(menuKey: String) {
activityView!!.showSettingsFragment(
menuKey,
true,
requireArguments().getString(ARGUMENT_GAME_ID)!!
)
}
override fun showToastMessage(message: String?, is_long: Boolean) {
activityView!!.showToastMessage(message!!, is_long)
}
override fun putSetting(setting: AbstractSetting) {
fragmentPresenter.putSetting(setting)
}
override fun onSettingChanged() {
activityView!!.onSettingChanged()
} }
private fun setInsets() { private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener( ViewCompat.setOnApplyWindowInsetsListener(
binding.listSettings binding.root
) { view: View, windowInsets: WindowInsetsCompat -> ) { _: View, windowInsets: WindowInsetsCompat ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updatePadding(bottom = insets.bottom) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
val sideMargin = resources.getDimensionPixelSize(R.dimen.spacing_medlarge)
val mlpSettingsList = binding.listSettings.layoutParams as MarginLayoutParams
mlpSettingsList.leftMargin = sideMargin + leftInsets
mlpSettingsList.rightMargin = sideMargin + rightInsets
binding.listSettings.layoutParams = mlpSettingsList
binding.listSettings.updatePadding(
bottom = barInsets.bottom
)
val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams
mlpAppBar.leftMargin = leftInsets
mlpAppBar.rightMargin = rightInsets
binding.appbarSettings.layoutParams = mlpAppBar
windowInsets windowInsets
} }
} }
companion object {
private const val ARGUMENT_MENU_TAG = "menu_tag"
private const val ARGUMENT_GAME_ID = "game_id"
fun newInstance(menuTag: String?, gameId: String?): Fragment {
val fragment = SettingsFragment()
val arguments = Bundle()
arguments.putString(ARGUMENT_MENU_TAG, menuTag)
arguments.putString(ARGUMENT_GAME_ID, gameId)
fragment.arguments = arguments
return fragment
}
}
} }

View File

@ -3,63 +3,66 @@
package org.yuzu.yuzu_emu.features.settings.ui package org.yuzu.yuzu_emu.features.settings.ui
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.text.TextUtils import android.text.TextUtils
import android.widget.Toast
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.LongSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.StringSetting import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
import org.yuzu.yuzu_emu.features.settings.model.view.* import org.yuzu.yuzu_emu.features.settings.model.view.*
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 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.ThemeHelper import org.yuzu.yuzu_emu.utils.NativeConfig
class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) { class SettingsFragmentPresenter(
private var menuTag: String? = null private val settingsViewModel: SettingsViewModel,
private lateinit var gameId: String private val adapter: SettingsAdapter,
private var settingsList: ArrayList<SettingsItem>? = null private var menuTag: String,
private var gameId: String
) {
private var settingsList = ArrayList<SettingsItem>()
private val settingsActivity get() = fragmentView.activityView as SettingsActivity private val preferences: SharedPreferences
private val settings get() = fragmentView.activityView!!.settings get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
private lateinit var preferences: SharedPreferences private val context: Context get() = YuzuApplication.appContext
fun onCreate(menuTag: String, gameId: String) { // Extension for populating settings list based on paired settings
this.gameId = gameId fun ArrayList<SettingsItem>.add(key: String) {
this.menuTag = menuTag val item = SettingsItem.settingsItems[key]!!
val pairedSettingKey = item.setting.pairedSettingKey
if (pairedSettingKey.isNotEmpty()) {
val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false)
if (!pairedSettingValue) return
}
add(item)
} }
fun onViewCreated() { fun onViewCreated() {
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
loadSettingsList() loadSettingsList()
} }
fun putSetting(setting: AbstractSetting) {
if (setting.section == null || setting.key == null) {
return
}
val section = settings.getSection(setting.section!!)!!
if (section.getSetting(setting.key!!) == null) {
section.putSetting(setting)
}
}
fun loadSettingsList() { fun loadSettingsList() {
if (!TextUtils.isEmpty(gameId)) { if (!TextUtils.isEmpty(gameId)) {
settingsActivity.setToolbarTitle("Game Settings: $gameId") settingsViewModel.setToolbarTitle(
context.getString(
R.string.advanced_settings_game,
gameId
)
)
} }
val sl = ArrayList<SettingsItem>() val sl = ArrayList<SettingsItem>()
if (menuTag == null) {
return
}
when (menuTag) { when (menuTag) {
SettingsFile.FILE_NAME_CONFIG -> addConfigSettings(sl) SettingsFile.FILE_NAME_CONFIG -> addConfigSettings(sl)
Settings.SECTION_GENERAL -> addGeneralSettings(sl) Settings.SECTION_GENERAL -> addGeneralSettings(sl)
@ -69,335 +72,104 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
Settings.SECTION_THEME -> addThemeSettings(sl) Settings.SECTION_THEME -> addThemeSettings(sl)
Settings.SECTION_DEBUG -> addDebugSettings(sl) Settings.SECTION_DEBUG -> addDebugSettings(sl)
else -> { else -> {
fragmentView.showToastMessage("Unimplemented menu", false) val context = YuzuApplication.appContext
Toast.makeText(
context,
context.getString(R.string.unimplemented_menu),
Toast.LENGTH_SHORT
).show()
return return
} }
} }
settingsList = sl settingsList = sl
fragmentView.showSettingsList(settingsList!!) adapter.submitList(settingsList)
} }
private fun addConfigSettings(sl: ArrayList<SettingsItem>) { private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.advanced_settings)) settingsViewModel.setToolbarTitle(context.getString(R.string.advanced_settings))
sl.apply { sl.apply {
add(SubmenuSetting(R.string.preferences_general, 0, Settings.SECTION_GENERAL))
add(SubmenuSetting(R.string.preferences_system, 0, Settings.SECTION_SYSTEM))
add(SubmenuSetting(R.string.preferences_graphics, 0, Settings.SECTION_RENDERER))
add(SubmenuSetting(R.string.preferences_audio, 0, Settings.SECTION_AUDIO))
add(SubmenuSetting(R.string.preferences_debug, 0, Settings.SECTION_DEBUG))
add( add(
SubmenuSetting( RunnableSetting(R.string.reset_to_default, 0, false) {
R.string.preferences_general, settingsViewModel.setShouldShowResetSettingsDialog(true)
0,
Settings.SECTION_GENERAL
)
)
add(
SubmenuSetting(
R.string.preferences_system,
0,
Settings.SECTION_SYSTEM
)
)
add(
SubmenuSetting(
R.string.preferences_graphics,
0,
Settings.SECTION_RENDERER
)
)
add(
SubmenuSetting(
R.string.preferences_audio,
0,
Settings.SECTION_AUDIO
)
)
add(
SubmenuSetting(
R.string.preferences_debug,
0,
Settings.SECTION_DEBUG
)
)
add(
RunnableSetting(
R.string.reset_to_default,
0,
false
) {
ResetSettingsDialogFragment().show(
settingsActivity.supportFragmentManager,
ResetSettingsDialogFragment.TAG
)
} }
) )
} }
} }
private fun addGeneralSettings(sl: ArrayList<SettingsItem>) { private fun addGeneralSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_general)) settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_general))
sl.apply { sl.apply {
add( add(BooleanSetting.RENDERER_USE_SPEED_LIMIT.key)
SwitchSetting( add(ShortSetting.RENDERER_SPEED_LIMIT.key)
IntSetting.RENDERER_USE_SPEED_LIMIT, add(IntSetting.CPU_ACCURACY.key)
R.string.frame_limit_enable, add(BooleanSetting.PICTURE_IN_PICTURE.key)
R.string.frame_limit_enable_description,
IntSetting.RENDERER_USE_SPEED_LIMIT.key,
IntSetting.RENDERER_USE_SPEED_LIMIT.defaultValue
)
)
add(
SliderSetting(
IntSetting.RENDERER_SPEED_LIMIT,
R.string.frame_limit_slider,
R.string.frame_limit_slider_description,
1,
200,
"%",
IntSetting.RENDERER_SPEED_LIMIT.key,
IntSetting.RENDERER_SPEED_LIMIT.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.CPU_ACCURACY,
R.string.cpu_accuracy,
0,
R.array.cpuAccuracyNames,
R.array.cpuAccuracyValues,
IntSetting.CPU_ACCURACY.key,
IntSetting.CPU_ACCURACY.defaultValue
)
)
add(
SwitchSetting(
BooleanSetting.PICTURE_IN_PICTURE,
R.string.picture_in_picture,
R.string.picture_in_picture_description,
BooleanSetting.PICTURE_IN_PICTURE.key,
BooleanSetting.PICTURE_IN_PICTURE.defaultValue
)
)
} }
} }
private fun addSystemSettings(sl: ArrayList<SettingsItem>) { private fun addSystemSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_system)) settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_system))
sl.apply { sl.apply {
add( add(BooleanSetting.USE_DOCKED_MODE.key)
SwitchSetting( add(IntSetting.REGION_INDEX.key)
IntSetting.USE_DOCKED_MODE, add(IntSetting.LANGUAGE_INDEX.key)
R.string.use_docked_mode, add(BooleanSetting.USE_CUSTOM_RTC.key)
R.string.use_docked_mode_description, add(LongSetting.CUSTOM_RTC.key)
IntSetting.USE_DOCKED_MODE.key,
IntSetting.USE_DOCKED_MODE.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.REGION_INDEX,
R.string.emulated_region,
0,
R.array.regionNames,
R.array.regionValues,
IntSetting.REGION_INDEX.key,
IntSetting.REGION_INDEX.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.LANGUAGE_INDEX,
R.string.emulated_language,
0,
R.array.languageNames,
R.array.languageValues,
IntSetting.LANGUAGE_INDEX.key,
IntSetting.LANGUAGE_INDEX.defaultValue
)
)
add(
SwitchSetting(
BooleanSetting.USE_CUSTOM_RTC,
R.string.use_custom_rtc,
R.string.use_custom_rtc_description,
BooleanSetting.USE_CUSTOM_RTC.key,
BooleanSetting.USE_CUSTOM_RTC.defaultValue
)
)
add(
DateTimeSetting(
StringSetting.CUSTOM_RTC,
R.string.set_custom_rtc,
0,
StringSetting.CUSTOM_RTC.key,
StringSetting.CUSTOM_RTC.defaultValue
)
)
} }
} }
private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) { private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics)) settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_graphics))
sl.apply { sl.apply {
add( add(IntSetting.RENDERER_ACCURACY.key)
SingleChoiceSetting( add(IntSetting.RENDERER_RESOLUTION.key)
IntSetting.RENDERER_ACCURACY, add(IntSetting.RENDERER_VSYNC.key)
R.string.renderer_accuracy, add(IntSetting.RENDERER_SCALING_FILTER.key)
0, add(IntSetting.RENDERER_ANTI_ALIASING.key)
R.array.rendererAccuracyNames, add(IntSetting.RENDERER_SCREEN_LAYOUT.key)
R.array.rendererAccuracyValues, add(IntSetting.RENDERER_ASPECT_RATIO.key)
IntSetting.RENDERER_ACCURACY.key, add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key)
IntSetting.RENDERER_ACCURACY.defaultValue add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key)
) add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
) add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key)
add(
SingleChoiceSetting(
IntSetting.RENDERER_RESOLUTION,
R.string.renderer_resolution,
0,
R.array.rendererResolutionNames,
R.array.rendererResolutionValues,
IntSetting.RENDERER_RESOLUTION.key,
IntSetting.RENDERER_RESOLUTION.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.RENDERER_VSYNC,
R.string.renderer_vsync,
0,
R.array.rendererVSyncNames,
R.array.rendererVSyncValues,
IntSetting.RENDERER_VSYNC.key,
IntSetting.RENDERER_VSYNC.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.RENDERER_SCALING_FILTER,
R.string.renderer_scaling_filter,
0,
R.array.rendererScalingFilterNames,
R.array.rendererScalingFilterValues,
IntSetting.RENDERER_SCALING_FILTER.key,
IntSetting.RENDERER_SCALING_FILTER.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.RENDERER_ANTI_ALIASING,
R.string.renderer_anti_aliasing,
0,
R.array.rendererAntiAliasingNames,
R.array.rendererAntiAliasingValues,
IntSetting.RENDERER_ANTI_ALIASING.key,
IntSetting.RENDERER_ANTI_ALIASING.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.RENDERER_SCREEN_LAYOUT,
R.string.renderer_screen_layout,
0,
R.array.rendererScreenLayoutNames,
R.array.rendererScreenLayoutValues,
IntSetting.RENDERER_SCREEN_LAYOUT.key,
IntSetting.RENDERER_SCREEN_LAYOUT.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.RENDERER_ASPECT_RATIO,
R.string.renderer_aspect_ratio,
0,
R.array.rendererAspectRatioNames,
R.array.rendererAspectRatioValues,
IntSetting.RENDERER_ASPECT_RATIO.key,
IntSetting.RENDERER_ASPECT_RATIO.defaultValue
)
)
add(
SwitchSetting(
IntSetting.RENDERER_USE_DISK_SHADER_CACHE,
R.string.use_disk_shader_cache,
R.string.use_disk_shader_cache_description,
IntSetting.RENDERER_USE_DISK_SHADER_CACHE.key,
IntSetting.RENDERER_USE_DISK_SHADER_CACHE.defaultValue
)
)
add(
SwitchSetting(
IntSetting.RENDERER_FORCE_MAX_CLOCK,
R.string.renderer_force_max_clock,
R.string.renderer_force_max_clock_description,
IntSetting.RENDERER_FORCE_MAX_CLOCK.key,
IntSetting.RENDERER_FORCE_MAX_CLOCK.defaultValue
)
)
add(
SwitchSetting(
IntSetting.RENDERER_ASYNCHRONOUS_SHADERS,
R.string.renderer_asynchronous_shaders,
R.string.renderer_asynchronous_shaders_description,
IntSetting.RENDERER_ASYNCHRONOUS_SHADERS.key,
IntSetting.RENDERER_ASYNCHRONOUS_SHADERS.defaultValue
)
)
add(
SwitchSetting(
IntSetting.RENDERER_REACTIVE_FLUSHING,
R.string.renderer_reactive_flushing,
R.string.renderer_reactive_flushing_description,
IntSetting.RENDERER_REACTIVE_FLUSHING.key,
IntSetting.RENDERER_REACTIVE_FLUSHING.defaultValue
)
)
} }
} }
private fun addAudioSettings(sl: ArrayList<SettingsItem>) { private fun addAudioSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_audio)) settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_audio))
sl.apply { sl.apply {
add( add(IntSetting.AUDIO_OUTPUT_ENGINE.key)
StringSingleChoiceSetting( add(ByteSetting.AUDIO_VOLUME.key)
StringSetting.AUDIO_OUTPUT_ENGINE,
R.string.audio_output_engine,
0,
settingsActivity.resources.getStringArray(R.array.outputEngineEntries),
settingsActivity.resources.getStringArray(R.array.outputEngineValues),
StringSetting.AUDIO_OUTPUT_ENGINE.key,
StringSetting.AUDIO_OUTPUT_ENGINE.defaultValue
)
)
add(
SliderSetting(
IntSetting.AUDIO_VOLUME,
R.string.audio_volume,
R.string.audio_volume_description,
0,
100,
"%",
IntSetting.AUDIO_VOLUME.key,
IntSetting.AUDIO_VOLUME.defaultValue
)
)
} }
} }
private fun addThemeSettings(sl: ArrayList<SettingsItem>) { private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_theme)) settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_theme))
sl.apply { sl.apply {
val theme: AbstractIntSetting = object : AbstractIntSetting { val theme: AbstractIntSetting = object : AbstractIntSetting {
override var int: Int override val int: Int
get() = preferences.getInt(Settings.PREF_THEME, 0) get() = preferences.getInt(Settings.PREF_THEME, 0)
set(value) {
override fun setInt(value: Int) {
preferences.edit() preferences.edit()
.putInt(Settings.PREF_THEME, value) .putInt(Settings.PREF_THEME, value)
.apply() .apply()
settingsActivity.recreate() settingsViewModel.setShouldRecreate(true)
}
override val key: String = Settings.PREF_THEME
override val category = Settings.Category.UiGeneral
override val isRuntimeModifiable: Boolean = false
override val defaultValue: Int = 0
override fun reset() {
preferences.edit()
.putInt(Settings.PREF_THEME, defaultValue)
.apply()
} }
override val key: String? = null
override val section: String? = null
override val isRuntimeEditable: Boolean = false
override val valueAsString: String
get() = preferences.getInt(Settings.PREF_THEME, 0).toString()
override val defaultValue: Any = 0
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -423,20 +195,26 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
} }
val themeMode: AbstractIntSetting = object : AbstractIntSetting { val themeMode: AbstractIntSetting = object : AbstractIntSetting {
override var int: Int override val int: Int
get() = preferences.getInt(Settings.PREF_THEME_MODE, -1) get() = preferences.getInt(Settings.PREF_THEME_MODE, -1)
set(value) {
override fun setInt(value: Int) {
preferences.edit() preferences.edit()
.putInt(Settings.PREF_THEME_MODE, value) .putInt(Settings.PREF_THEME_MODE, value)
.apply() .apply()
ThemeHelper.setThemeMode(settingsActivity) settingsViewModel.setShouldRecreate(true)
}
override val key: String = Settings.PREF_THEME_MODE
override val category = Settings.Category.UiGeneral
override val isRuntimeModifiable: Boolean = false
override val defaultValue: Int = -1
override fun reset() {
preferences.edit()
.putInt(Settings.PREF_BLACK_BACKGROUNDS, defaultValue)
.apply()
settingsViewModel.setShouldRecreate(true)
} }
override val key: String? = null
override val section: String? = null
override val isRuntimeEditable: Boolean = false
override val valueAsString: String
get() = preferences.getInt(Settings.PREF_THEME_MODE, -1).toString()
override val defaultValue: Any = -1
} }
add( add(
@ -450,21 +228,26 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
) )
val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting { val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
override var boolean: Boolean override val boolean: Boolean
get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
set(value) {
override fun setBoolean(value: Boolean) {
preferences.edit() preferences.edit()
.putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value) .putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value)
.apply() .apply()
settingsActivity.recreate() settingsViewModel.setShouldRecreate(true)
}
override val key: String = Settings.PREF_BLACK_BACKGROUNDS
override val category = Settings.Category.UiGeneral
override val isRuntimeModifiable: Boolean = false
override val defaultValue: Boolean = false
override fun reset() {
preferences.edit()
.putBoolean(Settings.PREF_BLACK_BACKGROUNDS, defaultValue)
.apply()
settingsViewModel.setShouldRecreate(true)
} }
override val key: String? = null
override val section: String? = null
override val isRuntimeEditable: Boolean = false
override val valueAsString: String
get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
.toString()
override val defaultValue: Any = false
} }
add( add(
@ -478,62 +261,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
} }
private fun addDebugSettings(sl: ArrayList<SettingsItem>) { private fun addDebugSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_debug)) settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_debug))
sl.apply { sl.apply {
add(HeaderSetting(R.string.gpu)) add(HeaderSetting(R.string.gpu))
add( add(IntSetting.RENDERER_BACKEND.key)
SingleChoiceSetting( add(BooleanSetting.RENDERER_DEBUG.key)
IntSetting.RENDERER_BACKEND,
R.string.renderer_api,
0,
R.array.rendererApiNames,
R.array.rendererApiValues,
IntSetting.RENDERER_BACKEND.key,
IntSetting.RENDERER_BACKEND.defaultValue
)
)
add(
SwitchSetting(
IntSetting.RENDERER_DEBUG,
R.string.renderer_debug,
R.string.renderer_debug_description,
IntSetting.RENDERER_DEBUG.key,
IntSetting.RENDERER_DEBUG.defaultValue
)
)
add(HeaderSetting(R.string.cpu)) add(HeaderSetting(R.string.cpu))
add( add(BooleanSetting.CPU_DEBUG_MODE.key)
SwitchSetting( add(SettingsItem.FASTMEM_COMBINED)
BooleanSetting.CPU_DEBUG_MODE,
R.string.cpu_debug_mode,
R.string.cpu_debug_mode_description,
BooleanSetting.CPU_DEBUG_MODE.key,
BooleanSetting.CPU_DEBUG_MODE.defaultValue
)
)
val fastmem = object : AbstractBooleanSetting {
override var boolean: Boolean
get() =
BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean
set(value) {
BooleanSetting.FASTMEM.boolean = value
BooleanSetting.FASTMEM_EXCLUSIVES.boolean = value
}
override val key: String? = null
override val section: String = Settings.SECTION_CPU
override val isRuntimeEditable: Boolean = false
override val valueAsString: String = ""
override val defaultValue: Any = true
}
add(
SwitchSetting(
fastmem,
R.string.fastmem,
0
)
)
} }
} }
} }

View File

@ -1,58 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
/**
* Abstraction for a screen showing a list of settings. Instances of
* this type of view will each display a layer of the setting hierarchy.
*/
interface SettingsFragmentView {
/**
* Pass an ArrayList to the View so that it can be displayed on screen.
*
* @param settingsList The result of converting the HashMap to an ArrayList
*/
fun showSettingsList(settingsList: ArrayList<SettingsItem>)
/**
* Instructs the Fragment to load the settings screen.
*/
fun loadSettingsList()
/**
* @return The Fragment's containing activity.
*/
val activityView: SettingsActivityView?
/**
* Tell the Fragment to tell the containing Activity to show a new
* Fragment containing a submenu of settings.
*
* @param menuKey Identifier for the settings group that should be shown.
*/
fun loadSubMenu(menuKey: String)
/**
* Tell the Fragment to tell the containing activity to display a toast message.
*
* @param message Text to be shown in the Toast
* @param is_long Whether this should be a long Toast or short one.
*/
fun showToastMessage(message: String?, is_long: Boolean)
/**
* Have the fragment add a setting to the HashMap.
*
* @param setting The (possibly previously missing) new setting.
*/
fun putSetting(setting: AbstractSetting)
/**
* Have the fragment tell the containing Activity that a setting was modified.
*/
fun onSettingChanged()
}

View File

@ -25,12 +25,17 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
binding.textSettingDescription.setText(item.descriptionId) binding.textSettingDescription.setText(item.descriptionId)
binding.textSettingDescription.visibility = View.VISIBLE binding.textSettingDescription.visibility = View.VISIBLE
} else { } else {
val epochTime = setting.value.toLong() binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.visibility = View.VISIBLE
val epochTime = setting.value
val instant = Instant.ofEpochMilli(epochTime * 1000) val instant = Instant.ofEpochMilli(epochTime * 1000)
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
binding.textSettingDescription.text = dateFormatter.format(zonedTime) binding.textSettingValue.text = dateFormatter.format(zonedTime)
}
setStyle(setting.isEditable, binding)
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
@ -41,7 +46,7 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) { if (setting.isEditable) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) return adapter.onLongClick(setting, bindingAdapterPosition)
} }
return false return false
} }

View File

@ -23,6 +23,9 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
} else { } else {
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE
} }
binding.textSettingValue.visibility = View.GONE
setStyle(setting.isEditable, binding)
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {

View File

@ -5,6 +5,8 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
@ -33,4 +35,18 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
abstract override fun onClick(clicked: View) abstract override fun onClick(clicked: View)
abstract override fun onLongClick(clicked: View): Boolean abstract override fun onLongClick(clicked: View): Boolean
fun setStyle(isEditable: Boolean, binding: ListItemSettingBinding) {
val opacity = if (isEditable) 1.0f else 0.5f
binding.textSettingName.alpha = opacity
binding.textSettingDescription.alpha = opacity
binding.textSettingValue.alpha = opacity
}
fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) {
binding.switchWidget.isEnabled = isEditable
val opacity = if (isEditable) 1.0f else 0.5f
binding.textSettingName.alpha = opacity
binding.textSettingDescription.alpha = opacity
}
} }

View File

@ -17,28 +17,33 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
override fun bind(item: SettingsItem) { override fun bind(item: SettingsItem) {
setting = item setting = item
binding.textSettingName.setText(item.nameId) binding.textSettingName.setText(item.nameId)
binding.textSettingDescription.visibility = View.VISIBLE
if (item.descriptionId != 0) { if (item.descriptionId != 0) {
binding.textSettingDescription.setText(item.descriptionId) binding.textSettingDescription.setText(item.descriptionId)
} else if (item is SingleChoiceSetting) { binding.textSettingDescription.visibility = View.VISIBLE
val resMgr = binding.textSettingDescription.context.resources
val values = resMgr.getIntArray(item.valuesId)
for (i in values.indices) {
if (values[i] == item.selectedValue) {
binding.textSettingDescription.text = resMgr.getStringArray(item.choicesId)[i]
return
}
}
} else if (item is StringSingleChoiceSetting) {
for (i in item.values!!.indices) {
if (item.values[i] == item.selectedValue) {
binding.textSettingDescription.text = item.choices[i]
return
}
}
} else { } else {
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE
} }
binding.textSettingValue.visibility = View.VISIBLE
if (item is SingleChoiceSetting) {
val resMgr = binding.textSettingValue.context.resources
val values = resMgr.getIntArray(item.valuesId)
for (i in values.indices) {
if (values[i] == item.selectedValue) {
binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i]
break
}
}
} else if (item is StringSingleChoiceSetting) {
for (i in item.values.indices) {
if (item.values[i] == item.selectedValue) {
binding.textSettingValue.text = item.choices[i]
break
}
}
}
setStyle(setting.isEditable, binding)
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
@ -61,7 +66,7 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) { if (setting.isEditable) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) return adapter.onLongClick(setting, bindingAdapterPosition)
} }
return false return false
} }

View File

@ -4,6 +4,7 @@
package org.yuzu.yuzu_emu.features.settings.ui.viewholder package org.yuzu.yuzu_emu.features.settings.ui.viewholder
import android.view.View import android.view.View
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
@ -22,6 +23,14 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
} else { } else {
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE
} }
binding.textSettingValue.visibility = View.VISIBLE
binding.textSettingValue.text = String.format(
binding.textSettingValue.context.getString(R.string.value_with_units),
setting.selectedValue,
setting.units
)
setStyle(setting.isEditable, binding)
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
@ -32,7 +41,7 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) { if (setting.isEditable) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) return adapter.onLongClick(setting, bindingAdapterPosition)
} }
return false return false
} }

View File

@ -22,6 +22,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
} else { } else {
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE
} }
binding.textSettingValue.visibility = View.GONE
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {

View File

@ -25,12 +25,14 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
binding.textSettingDescription.text = "" binding.textSettingDescription.text = ""
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE
} }
binding.switchWidget.isChecked = setting.isChecked
binding.switchWidget.setOnCheckedChangeListener(null)
binding.switchWidget.isChecked = setting.checked
binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
adapter.onBooleanClick(item, bindingAdapterPosition, binding.switchWidget.isChecked) adapter.onBooleanClick(item, binding.switchWidget.isChecked)
} }
binding.switchWidget.isEnabled = setting.isEditable setStyle(setting.isEditable, binding)
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
@ -41,7 +43,7 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) { if (setting.isEditable) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) return adapter.onLongClick(setting, bindingAdapterPosition)
} }
return false return false
} }

View File

@ -3,18 +3,15 @@
package org.yuzu.yuzu_emu.features.settings.utils package org.yuzu.yuzu_emu.features.settings.utils
import android.widget.Toast
import java.io.* import java.io.*
import java.util.*
import org.ini4j.Wini import org.ini4j.Wini
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.* import org.yuzu.yuzu_emu.features.settings.model.*
import org.yuzu.yuzu_emu.features.settings.model.Settings.SettingsSectionMap
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView
import org.yuzu.yuzu_emu.utils.BiMap
import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.NativeConfig
/** /**
* Contains static methods for interacting with .ini files in which settings are stored. * Contains static methods for interacting with .ini files in which settings are stored.
@ -22,243 +19,41 @@ import org.yuzu.yuzu_emu.utils.Log
object SettingsFile { object SettingsFile {
const val FILE_NAME_CONFIG = "config" const val FILE_NAME_CONFIG = "config"
private var sectionsMap = BiMap<String?, String?>()
/**
* Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves
* effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
* failed.
*
* @param ini The ini file to load the settings from
* @param isCustomGame
* @param view The current view.
* @return An Observable that emits a HashMap of the file's contents, then completes.
*/
private fun readFile(
ini: File?,
isCustomGame: Boolean,
view: SettingsActivityView? = null
): HashMap<String, SettingSection?> {
val sections: HashMap<String, SettingSection?> = SettingsSectionMap()
var reader: BufferedReader? = null
try {
reader = BufferedReader(FileReader(ini))
var current: SettingSection? = null
var line: String?
while (reader.readLine().also { line = it } != null) {
if (line!!.startsWith("[") && line!!.endsWith("]")) {
current = sectionFromLine(line!!, isCustomGame)
sections[current.name] = current
} else if (current != null) {
val setting = settingFromLine(line!!)
if (setting != null) {
current.putSetting(setting)
}
}
}
} catch (e: FileNotFoundException) {
Log.error("[SettingsFile] File not found: " + e.message)
view?.onSettingsFileNotFound()
} catch (e: IOException) {
Log.error("[SettingsFile] Error reading from: " + e.message)
view?.onSettingsFileNotFound()
} finally {
if (reader != null) {
try {
reader.close()
} catch (e: IOException) {
Log.error("[SettingsFile] Error closing: " + e.message)
}
}
}
return sections
}
fun readFile(fileName: String, view: SettingsActivityView?): HashMap<String, SettingSection?> {
return readFile(getSettingsFile(fileName), false, view)
}
fun readFile(fileName: String): HashMap<String, SettingSection?> =
readFile(getSettingsFile(fileName), false)
/**
* Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves
* effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
* failed.
*
* @param gameId the id of the game to load it's settings.
* @param view The current view.
*/
fun readCustomGameSettings(
gameId: String,
view: SettingsActivityView?
): HashMap<String, SettingSection?> {
return readFile(getCustomGameSettingsFile(gameId), true, view)
}
/** /**
* Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error
* telling why it failed. * telling why it failed.
* *
* @param fileName The target filename without a path or extension. * @param fileName The target filename without a path or extension.
* @param sections The HashMap containing the Settings we want to serialize.
* @param view The current view.
*/ */
fun saveFile( fun saveFile(fileName: String) {
fileName: String,
sections: TreeMap<String, SettingSection>,
view: SettingsActivityView
) {
val ini = getSettingsFile(fileName) val ini = getSettingsFile(fileName)
try { try {
val writer = Wini(ini) val wini = Wini(ini)
val keySet: Set<String> = sections.keys for (specificCategory in Settings.Category.values()) {
for (key in keySet) { val categoryHeader = NativeConfig.getConfigHeader(specificCategory.ordinal)
val section = sections[key] for (setting in Settings.settingsList) {
writeSection(writer, section!!) if (setting.key!!.isEmpty()) continue
val settingCategoryHeader =
NativeConfig.getConfigHeader(setting.category.ordinal)
val iniSetting: String? = wini.get(categoryHeader, setting.key)
if (iniSetting != null || settingCategoryHeader == categoryHeader) {
wini.put(settingCategoryHeader, setting.key, setting.valueAsString)
} }
writer.store() }
}
wini.store()
} catch (e: IOException) { } catch (e: IOException) {
Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.message) Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.message)
view.showToastMessage( val context = YuzuApplication.appContext
YuzuApplication.appContext Toast.makeText(
.getString(R.string.error_saving, fileName, e.message), context,
false context.getString(R.string.error_saving, fileName, e.message),
) Toast.LENGTH_SHORT
).show()
} }
} }
fun saveCustomGameSettings(gameId: String?, sections: HashMap<String, SettingSection?>) { fun getSettingsFile(fileName: String): File =
val sortedSections: Set<String> = TreeSet(sections.keys) File(DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini")
for (sectionKey in sortedSections) {
val section = sections[sectionKey]
val settings = section!!.settings
val sortedKeySet: Set<String> = TreeSet(settings.keys)
for (settingKey in sortedKeySet) {
val setting = settings[settingKey]
NativeLibrary.setUserSetting(
gameId,
mapSectionNameFromIni(
section.name
),
setting!!.key,
setting.valueAsString
)
}
}
}
private fun mapSectionNameFromIni(generalSectionName: String): String? {
return if (sectionsMap.getForward(generalSectionName) != null) {
sectionsMap.getForward(generalSectionName)
} else {
generalSectionName
}
}
private fun mapSectionNameToIni(generalSectionName: String): String {
return if (sectionsMap.getBackward(generalSectionName) != null) {
sectionsMap.getBackward(generalSectionName).toString()
} else {
generalSectionName
}
}
fun getSettingsFile(fileName: String): File {
return File(
DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini"
)
}
private fun getCustomGameSettingsFile(gameId: String): File {
return File(DirectoryInitialization.userDirectory + "/GameSettings/" + gameId + ".ini")
}
private fun sectionFromLine(line: String, isCustomGame: Boolean): SettingSection {
var sectionName: String = line.substring(1, line.length - 1)
if (isCustomGame) {
sectionName = mapSectionNameToIni(sectionName)
}
return SettingSection(sectionName)
}
/**
* For a line of text, determines what type of data is being represented, and returns
* a Setting object containing this data.
*
* @param line The line of text being parsed.
* @return A typed Setting containing the key/value contained in the line.
*/
private fun settingFromLine(line: String): AbstractSetting? {
val splitLine = line.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (splitLine.size != 2) {
return null
}
val key = splitLine[0].trim { it <= ' ' }
val value = splitLine[1].trim { it <= ' ' }
if (value.isEmpty()) {
return null
}
val booleanSetting = BooleanSetting.from(key)
if (booleanSetting != null) {
booleanSetting.boolean = value.toBoolean()
return booleanSetting
}
val intSetting = IntSetting.from(key)
if (intSetting != null) {
intSetting.int = value.toInt()
return intSetting
}
val floatSetting = FloatSetting.from(key)
if (floatSetting != null) {
floatSetting.float = value.toFloat()
return floatSetting
}
val stringSetting = StringSetting.from(key)
if (stringSetting != null) {
stringSetting.string = value
return stringSetting
}
return null
}
/**
* Writes the contents of a Section HashMap to disk.
*
* @param parser A Wini pointed at a file on disk.
* @param section A section containing settings to be written to the file.
*/
private fun writeSection(parser: Wini, section: SettingSection) {
// Write the section header.
val header = section.name
// Write this section's values.
val settings = section.settings
val keySet: Set<String> = settings.keys
for (key in keySet) {
val setting = settings[key]
parser.put(header, setting!!.key, setting.valueAsString)
}
BooleanSetting.values().forEach {
if (!keySet.contains(it.key)) {
parser.put(header, it.key, it.valueAsString)
}
}
IntSetting.values().forEach {
if (!keySet.contains(it.key)) {
parser.put(header, it.key, it.valueAsString)
}
}
StringSetting.values().forEach {
if (!keySet.contains(it.key)) {
parser.put(header, it.key, it.valueAsString)
}
}
}
} }

View File

@ -7,11 +7,11 @@ import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@ -19,18 +19,18 @@ import android.util.Rational
import android.view.* import android.view.*
import android.widget.TextView import android.widget.TextView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.window.layout.FoldingFeature import androidx.window.layout.FoldingFeature
@ -40,6 +40,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
@ -48,8 +49,9 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.EmulationViewModel
import org.yuzu.yuzu_emu.overlay.InputOverlay import org.yuzu.yuzu_emu.overlay.InputOverlay
import org.yuzu.yuzu_emu.utils.* import org.yuzu.yuzu_emu.utils.*
@ -62,12 +64,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private var _binding: FragmentEmulationBinding? = null private var _binding: FragmentEmulationBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
val args by navArgs<EmulationFragmentArgs>() private val args by navArgs<EmulationFragmentArgs>()
private lateinit var game: Game
private val emulationViewModel: EmulationViewModel by activityViewModels()
private var isInFoldableLayout = false private var isInFoldableLayout = false
private lateinit var onReturnFromSettings: ActivityResultLauncher<Intent>
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
if (context is EmulationActivity) { if (context is EmulationActivity) {
@ -81,11 +85,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
.collect { updateFoldableLayout(context, it) } .collect { updateFoldableLayout(context, it) }
} }
} }
onReturnFromSettings = context.activityResultRegistry.register(
"SettingsResult",
ActivityResultContracts.StartActivityForResult()
) { updateScreenLayout() }
} else { } else {
throw IllegalStateException("EmulationFragment must have EmulationActivity parent") throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
} }
@ -97,10 +96,25 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val intentUri: Uri? = requireActivity().intent.data
var intentGame: Game? = null
if (intentUri != null) {
intentGame = if (Game.extensions.contains(FileUtil.getExtension(intentUri))) {
GameHelper.getGame(requireActivity().intent.data!!, false)
} else {
null
}
}
game = if (args.game != null) {
args.game!!
} else {
intentGame ?: error("[EmulationFragment] No bootable game present!")
}
// So this fragment doesn't restart on configuration changes; i.e. rotation. // So this fragment doesn't restart on configuration changes; i.e. rotation.
retainInstance = true retainInstance = true
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
emulationState = EmulationState(args.game.path) emulationState = EmulationState(game.path)
} }
/** /**
@ -120,11 +134,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.showFpsText.setTextColor(Color.YELLOW) binding.showFpsText.setTextColor(Color.YELLOW)
binding.doneControlConfig.setOnClickListener { stopConfiguringControls() } binding.doneControlConfig.setOnClickListener { stopConfiguringControls() }
// Setup overlay. binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
updateShowFpsOverlay()
binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text = binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
args.game.title game.title
binding.inGameMenu.setNavigationItemSelectedListener { binding.inGameMenu.setNavigationItemSelectedListener {
when (it.itemId) { when (it.itemId) {
R.id.menu_pause_emulation -> { R.id.menu_pause_emulation -> {
@ -149,12 +161,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
R.id.menu_settings -> { R.id.menu_settings -> {
SettingsActivity.launch( val action = HomeNavigationDirections.actionGlobalSettingsActivity(
requireContext(), null,
onReturnFromSettings, SettingsFile.FILE_NAME_CONFIG
SettingsFile.FILE_NAME_CONFIG,
""
) )
binding.root.findNavController().navigate(action)
true true
} }
@ -165,7 +176,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
R.id.menu_exit -> { R.id.menu_exit -> {
emulationState.stop() emulationState.stop()
requireActivity().finish() emulationViewModel.setIsEmulationStopping(true)
binding.drawerLayout.close()
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
true true
} }
@ -179,6 +192,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
requireActivity(), requireActivity(),
object : OnBackPressedCallback(true) { object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
if (!NativeLibrary.isRunning()) {
return
}
if (binding.drawerLayout.isOpen) { if (binding.drawerLayout.isOpen) {
binding.drawerLayout.close() binding.drawerLayout.close()
} else { } else {
@ -195,6 +212,54 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
.collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) } .collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) }
} }
} }
GameIconUtils.loadGameIcon(game, binding.loadingImage)
binding.loadingTitle.text = game.title
binding.loadingTitle.isSelected = true
binding.loadingText.isSelected = true
emulationViewModel.shaderProgress.observe(viewLifecycleOwner) {
if (it > 0 && it != emulationViewModel.totalShaders.value!!) {
binding.loadingProgressIndicator.isIndeterminate = false
if (it < binding.loadingProgressIndicator.max) {
binding.loadingProgressIndicator.progress = it
}
}
if (it == emulationViewModel.totalShaders.value!!) {
binding.loadingText.setText(R.string.loading)
binding.loadingProgressIndicator.isIndeterminate = true
}
}
emulationViewModel.totalShaders.observe(viewLifecycleOwner) {
binding.loadingProgressIndicator.max = it
}
emulationViewModel.shaderMessage.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.loadingText.text = it
}
}
emulationViewModel.emulationStarted.observe(viewLifecycleOwner) { started ->
if (started) {
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
ViewUtils.showView(binding.surfaceInputOverlay)
ViewUtils.hideView(binding.loadingIndicator)
// Setup overlay
updateShowFpsOverlay()
}
}
emulationViewModel.isEmulationStopping.observe(viewLifecycleOwner) {
if (it) {
binding.loadingText.setText(R.string.shutting_down)
ViewUtils.showView(binding.loadingIndicator)
ViewUtils.hideView(binding.inputContainer)
ViewUtils.hideView(binding.showFpsText)
}
}
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
@ -204,11 +269,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.drawerLayout.close() binding.drawerLayout.close()
} }
if (EmulationMenuSettings.showOverlay) { if (EmulationMenuSettings.showOverlay) {
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false } binding.surfaceInputOverlay.post {
binding.surfaceInputOverlay.visibility = View.VISIBLE
}
} }
} else { } else {
if (EmulationMenuSettings.showOverlay) { if (EmulationMenuSettings.showOverlay &&
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true } emulationViewModel.emulationStarted.value == true
) {
binding.surfaceInputOverlay.post {
binding.surfaceInputOverlay.visibility = View.VISIBLE
}
} else {
binding.surfaceInputOverlay.post {
binding.surfaceInputOverlay.visibility = View.INVISIBLE
}
} }
if (!isInFoldableLayout) { if (!isInFoldableLayout) {
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
@ -217,16 +292,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.surfaceInputOverlay.layout = InputOverlay.LANDSCAPE binding.surfaceInputOverlay.layout = InputOverlay.LANDSCAPE
} }
} }
if (!binding.surfaceInputOverlay.isInEditMode) {
refreshInputOverlay()
}
} }
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (!DirectoryInitialization.areDirectoriesReady) { if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start(requireContext()) DirectoryInitialization.start()
} }
updateScreenLayout() updateScreenLayout()
@ -251,10 +323,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
super.onDetach() super.onDetach()
} }
private fun refreshInputOverlay() {
binding.surfaceInputOverlay.refreshControls()
}
private fun resetInputOverlay() { private fun resetInputOverlay() {
preferences.edit() preferences.edit()
.remove(Settings.PREF_CONTROL_SCALE) .remove(Settings.PREF_CONTROL_SCALE)
@ -272,17 +340,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
val FRAMETIME = 2 val FRAMETIME = 2
val SPEED = 3 val SPEED = 3
perfStatsUpdater = { perfStatsUpdater = {
if (emulationViewModel.emulationStarted.value == true) {
val perfStats = NativeLibrary.getPerfStats() val perfStats = NativeLibrary.getPerfStats()
if (perfStats[FPS] > 0 && _binding != null) { if (perfStats[FPS] > 0 && _binding != null) {
binding.showFpsText.text = String.format("FPS: %.1f", perfStats[FPS]) binding.showFpsText.text = String.format("FPS: %.1f", perfStats[FPS])
} }
if (!emulationState.isStopped) {
perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 100) perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 100)
} }
} }
perfStatsUpdateHandler.post(perfStatsUpdater!!) perfStatsUpdateHandler.post(perfStatsUpdater!!)
binding.showFpsText.text = resources.getString(R.string.emulation_game_loading)
binding.showFpsText.visibility = View.VISIBLE binding.showFpsText.visibility = View.VISIBLE
} else { } else {
if (perfStatsUpdater != null) { if (perfStatsUpdater != null) {
@ -297,11 +363,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
emulationActivity?.let { emulationActivity?.let {
it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) { it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) {
Settings.LayoutOption_MobileLandscape -> Settings.LayoutOption_MobileLandscape ->
ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
Settings.LayoutOption_MobilePortrait -> Settings.LayoutOption_MobilePortrait ->
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
Settings.LayoutOption_Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED Settings.LayoutOption_Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE else -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
} }
} }
} }
@ -340,7 +406,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
isInFoldableLayout = true isInFoldableLayout = true
binding.surfaceInputOverlay.layout = InputOverlay.FOLDABLE binding.surfaceInputOverlay.layout = InputOverlay.FOLDABLE
refreshInputOverlay()
} }
} }
it.isSeparating it.isSeparating
@ -428,7 +493,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
.apply() .apply()
} }
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
refreshInputOverlay() binding.surfaceInputOverlay.refreshControls()
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.emulation_toggle_all) { _, _ -> } .setNeutralButton(R.string.emulation_toggle_all) { _, _ -> }
@ -452,7 +517,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
R.id.menu_show_overlay -> { R.id.menu_show_overlay -> {
it.isChecked = !it.isChecked it.isChecked = !it.isChecked
EmulationMenuSettings.showOverlay = it.isChecked EmulationMenuSettings.showOverlay = it.isChecked
refreshInputOverlay() binding.surfaceInputOverlay.refreshControls()
true true
} }
@ -558,14 +623,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
preferences.edit() preferences.edit()
.putInt(Settings.PREF_CONTROL_SCALE, scale) .putInt(Settings.PREF_CONTROL_SCALE, scale)
.apply() .apply()
refreshInputOverlay() binding.surfaceInputOverlay.refreshControls()
} }
private fun setControlOpacity(opacity: Int) { private fun setControlOpacity(opacity: Int) {
preferences.edit() preferences.edit()
.putInt(Settings.PREF_CONTROL_OPACITY, opacity) .putInt(Settings.PREF_CONTROL_OPACITY, opacity)
.apply() .apply()
refreshInputOverlay() binding.surfaceInputOverlay.refreshControls()
} }
private fun setInsets() { private fun setInsets() {

View File

@ -25,17 +25,18 @@ import androidx.core.view.updatePadding
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.BuildConfig import org.yuzu.yuzu_emu.BuildConfig
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
import org.yuzu.yuzu_emu.features.DocumentProvider import org.yuzu.yuzu_emu.features.DocumentProvider
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.model.HomeSetting import org.yuzu.yuzu_emu.model.HomeSetting
import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.HomeViewModel
@ -74,7 +75,13 @@ class HomeSettingsFragment : Fragment() {
R.string.advanced_settings, R.string.advanced_settings,
R.string.settings_description, R.string.settings_description,
R.drawable.ic_settings, R.drawable.ic_settings,
{ SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") } {
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null,
SettingsFile.FILE_NAME_CONFIG
)
binding.root.findNavController().navigate(action)
}
) )
) )
add( add(
@ -90,7 +97,13 @@ class HomeSettingsFragment : Fragment() {
R.string.preferences_theme, R.string.preferences_theme,
R.string.theme_and_color_description, R.string.theme_and_color_description,
R.drawable.ic_palette, R.drawable.ic_palette,
{ SettingsActivity.launch(requireContext(), Settings.SECTION_THEME, "") } {
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null,
Settings.SECTION_THEME
)
binding.root.findNavController().navigate(action)
}
) )
) )
add( add(
@ -129,7 +142,11 @@ class HomeSettingsFragment : Fragment() {
mainActivity.getGamesDirectory.launch( mainActivity.getGamesDirectory.launch(
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
) )
} },
{ true },
0,
0,
homeViewModel.gamesDir
) )
) )
add( add(
@ -201,7 +218,11 @@ class HomeSettingsFragment : Fragment() {
binding.homeSettingsList.apply { binding.homeSettingsList.apply {
layoutManager = LinearLayoutManager(requireContext()) layoutManager = LinearLayoutManager(requireContext())
adapter = HomeSettingAdapter(requireActivity() as AppCompatActivity, optionsList) adapter = HomeSettingAdapter(
requireActivity() as AppCompatActivity,
viewLifecycleOwner,
optionsList
)
} }
setInsets() setInsets()

View File

@ -187,8 +187,8 @@ class ImportExportSavesFragment : DialogFragment() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (!validZip) { if (!validZip) {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
R.string.save_file_invalid_zip_structure, titleId = R.string.save_file_invalid_zip_structure,
R.string.save_file_invalid_zip_structure_description descriptionId = R.string.save_file_invalid_zip_structure_description
).show(activity.supportFragmentManager, MessageDialogFragment.TAG) ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
return@withContext return@withContext
} }

View File

@ -34,7 +34,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
when (val result = taskViewModel.result.value) { when (val result = taskViewModel.result.value) {
is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG).show() is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG).show()
is MessageDialogFragment -> result.show( is MessageDialogFragment -> result.show(
parentFragmentManager, requireActivity().supportFragmentManager,
MessageDialogFragment.TAG MessageDialogFragment.TAG
) )
} }

View File

@ -1,62 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.app.Dialog
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
class LongMessageDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val titleId = requireArguments().getInt(TITLE)
val description = requireArguments().getString(DESCRIPTION)
val helpLinkId = requireArguments().getInt(HELP_LINK)
val dialog = MaterialAlertDialogBuilder(requireContext())
.setPositiveButton(R.string.close, null)
.setTitle(titleId)
.setMessage(description)
if (helpLinkId != 0) {
dialog.setNeutralButton(R.string.learn_more) { _, _ ->
openLink(getString(helpLinkId))
}
}
return dialog.show()
}
private fun openLink(link: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
startActivity(intent)
}
companion object {
const val TAG = "LongMessageDialogFragment"
private const val TITLE = "Title"
private const val DESCRIPTION = "Description"
private const val HELP_LINK = "Link"
fun newInstance(
titleId: Int,
description: String,
helpLinkId: Int = 0
): LongMessageDialogFragment {
val dialog = LongMessageDialogFragment()
val bundle = Bundle()
bundle.apply {
putInt(TITLE, titleId)
putString(DESCRIPTION, description)
putInt(HELP_LINK, helpLinkId)
}
dialog.arguments = bundle
return dialog
}
}
}

View File

@ -13,14 +13,20 @@ import org.yuzu.yuzu_emu.R
class MessageDialogFragment : DialogFragment() { class MessageDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val titleId = requireArguments().getInt(TITLE) val titleId = requireArguments().getInt(TITLE_ID)
val descriptionId = requireArguments().getInt(DESCRIPTION) val titleString = requireArguments().getString(TITLE_STRING)!!
val descriptionId = requireArguments().getInt(DESCRIPTION_ID)
val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!!
val helpLinkId = requireArguments().getInt(HELP_LINK) val helpLinkId = requireArguments().getInt(HELP_LINK)
val dialog = MaterialAlertDialogBuilder(requireContext()) val dialog = MaterialAlertDialogBuilder(requireContext())
.setPositiveButton(R.string.close, null) .setPositiveButton(R.string.close, null)
.setTitle(titleId)
.setMessage(descriptionId) if (titleId != 0) dialog.setTitle(titleId)
if (titleString.isNotEmpty()) dialog.setTitle(titleString)
if (descriptionId != 0) dialog.setMessage(descriptionId)
if (descriptionString.isNotEmpty()) dialog.setMessage(descriptionString)
if (helpLinkId != 0) { if (helpLinkId != 0) {
dialog.setNeutralButton(R.string.learn_more) { _, _ -> dialog.setNeutralButton(R.string.learn_more) { _, _ ->
@ -39,20 +45,26 @@ class MessageDialogFragment : DialogFragment() {
companion object { companion object {
const val TAG = "MessageDialogFragment" const val TAG = "MessageDialogFragment"
private const val TITLE = "Title" private const val TITLE_ID = "Title"
private const val DESCRIPTION = "Description" private const val TITLE_STRING = "TitleString"
private const val DESCRIPTION_ID = "DescriptionId"
private const val DESCRIPTION_STRING = "DescriptionString"
private const val HELP_LINK = "Link" private const val HELP_LINK = "Link"
fun newInstance( fun newInstance(
titleId: Int, titleId: Int = 0,
descriptionId: Int, titleString: String = "",
descriptionId: Int = 0,
descriptionString: String = "",
helpLinkId: Int = 0 helpLinkId: Int = 0
): MessageDialogFragment { ): MessageDialogFragment {
val dialog = MessageDialogFragment() val dialog = MessageDialogFragment()
val bundle = Bundle() val bundle = Bundle()
bundle.apply { bundle.apply {
putInt(TITLE, titleId) putInt(TITLE_ID, titleId)
putInt(DESCRIPTION, descriptionId) putString(TITLE_STRING, titleString)
putInt(DESCRIPTION_ID, descriptionId)
putString(DESCRIPTION_STRING, descriptionString)
putInt(HELP_LINK, helpLinkId) putInt(HELP_LINK, helpLinkId)
} }
dialog.arguments = bundle dialog.arguments = bundle

View File

@ -0,0 +1,235 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogSliderBinding
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
import org.yuzu.yuzu_emu.model.SettingsViewModel
class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener {
private var type = 0
private var position = 0
private var defaultCancelListener =
DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
private val settingsViewModel: SettingsViewModel by activityViewModels()
private lateinit var sliderBinding: DialogSliderBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
type = requireArguments().getInt(TYPE)
position = requireArguments().getInt(POSITION)
if (settingsViewModel.clickedItem == null) dismiss()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return when (type) {
TYPE_RESET_SETTING -> {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.reset_setting_confirmation)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
settingsViewModel.clickedItem!!.setting.reset()
settingsViewModel.setAdapterItemChanged(position)
settingsViewModel.shouldSave = true
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
SettingsItem.TYPE_SINGLE_CHOICE -> {
val item = settingsViewModel.clickedItem as SingleChoiceSetting
val value = getSelectionForSingleChoiceValue(item)
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.nameId)
.setSingleChoiceItems(item.choicesId, value, this)
.create()
}
SettingsItem.TYPE_SLIDER -> {
sliderBinding = DialogSliderBinding.inflate(layoutInflater)
val item = settingsViewModel.clickedItem as SliderSetting
settingsViewModel.setSliderTextValue(item.selectedValue.toFloat(), item.units)
sliderBinding.slider.apply {
valueFrom = item.min.toFloat()
valueTo = item.max.toFloat()
value = settingsViewModel.sliderProgress.value.toFloat()
addOnChangeListener { _: Slider, value: Float, _: Boolean ->
settingsViewModel.setSliderTextValue(value, item.units)
}
}
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.nameId)
.setView(sliderBinding.root)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
.create()
}
SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
val item = settingsViewModel.clickedItem as StringSingleChoiceSetting
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.nameId)
.setSingleChoiceItems(item.choices, item.selectValueIndex, this)
.create()
}
else -> super.onCreateDialog(savedInstanceState)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return when (type) {
SettingsItem.TYPE_SLIDER -> sliderBinding.root
else -> super.onCreateView(inflater, container, savedInstanceState)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
when (type) {
SettingsItem.TYPE_SLIDER -> {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.sliderTextValue.collect {
sliderBinding.textValue.text = it
}
}
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.sliderProgress.collect {
sliderBinding.slider.value = it.toFloat()
}
}
}
}
}
}
override fun onClick(dialog: DialogInterface, which: Int) {
when (settingsViewModel.clickedItem) {
is SingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
val value = getValueForSingleChoiceSelection(scSetting, which)
if (scSetting.selectedValue != value) {
settingsViewModel.shouldSave = true
}
scSetting.selectedValue = value
}
is StringSingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
val value = scSetting.getValueAt(which)
if (scSetting.selectedValue != value) settingsViewModel.shouldSave = true
scSetting.selectedValue = value
}
is SliderSetting -> {
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
if (sliderSetting.selectedValue != settingsViewModel.sliderProgress.value) {
settingsViewModel.shouldSave = true
}
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
}
}
closeDialog()
}
private fun closeDialog() {
settingsViewModel.setAdapterItemChanged(position)
settingsViewModel.clickedItem = null
settingsViewModel.setSliderProgress(-1f)
dismiss()
}
private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int {
val valuesId = item.valuesId
return if (valuesId > 0) {
val valuesArray = requireContext().resources.getIntArray(valuesId)
valuesArray[which]
} else {
which
}
}
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
val value = item.selectedValue
val valuesId = item.valuesId
if (valuesId > 0) {
val valuesArray = requireContext().resources.getIntArray(valuesId)
for (index in valuesArray.indices) {
val current = valuesArray[index]
if (current == value) {
return index
}
}
} else {
return value
}
return -1
}
companion object {
const val TAG = "SettingsDialogFragment"
const val TYPE_RESET_SETTING = -1
const val TITLE = "Title"
const val TYPE = "Type"
const val POSITION = "Position"
fun newInstance(
settingsViewModel: SettingsViewModel,
clickedItem: SettingsItem,
type: Int,
position: Int
): SettingsDialogFragment {
when (type) {
SettingsItem.TYPE_HEADER,
SettingsItem.TYPE_SWITCH,
SettingsItem.TYPE_SUBMENU,
SettingsItem.TYPE_DATETIME_SETTING,
SettingsItem.TYPE_RUNNABLE ->
throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!")
SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress(
(clickedItem as SliderSetting).selectedValue.toFloat()
)
}
settingsViewModel.clickedItem = clickedItem
val args = Bundle()
args.putInt(TYPE, type)
args.putInt(POSITION, position)
val fragment = SettingsDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -0,0 +1,184 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.divider.MaterialDividerItemDecoration
import com.google.android.material.transition.MaterialSharedAxis
import info.debatty.java.stringsimilarity.Cosine
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.model.SettingsViewModel
import org.yuzu.yuzu_emu.utils.NativeConfig
class SettingsSearchFragment : Fragment() {
private var _binding: FragmentSettingsSearchBinding? = null
private val binding get() = _binding!!
private var settingsAdapter: SettingsAdapter? = null
private val settingsViewModel: SettingsViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSettingsSearchBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsViewModel.setIsUsingSearch(true)
if (savedInstanceState != null) {
binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
}
settingsAdapter = SettingsAdapter(this, requireContext())
val dividerDecoration = MaterialDividerItemDecoration(
requireContext(),
LinearLayoutManager.VERTICAL
)
dividerDecoration.isLastItemDecorated = false
binding.settingsList.apply {
adapter = settingsAdapter
layoutManager = LinearLayoutManager(requireContext())
addItemDecoration(dividerDecoration)
}
focusSearch()
binding.backButton.setOnClickListener { settingsViewModel.setShouldNavigateBack(true) }
binding.searchBackground.setOnClickListener { focusSearch() }
binding.clearButton.setOnClickListener { binding.searchText.setText("") }
binding.searchText.doOnTextChanged { _, _, _, _ ->
search()
binding.settingsList.smoothScrollToPosition(0)
}
settingsViewModel.shouldReloadSettingsList.observe(viewLifecycleOwner) {
if (it) {
settingsViewModel.setShouldReloadSettingsList(false)
search()
}
}
search()
setInsets()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
}
private fun search() {
val searchTerm = binding.searchText.text.toString().lowercase()
binding.clearButton.visibility =
if (searchTerm.isEmpty()) View.INVISIBLE else View.VISIBLE
if (searchTerm.isEmpty()) {
binding.noResultsView.visibility = View.VISIBLE
settingsAdapter?.submitList(emptyList())
return
}
val baseList = SettingsItem.settingsItems
val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1)
val sortedList: List<SettingsItem> = baseList.mapNotNull { item ->
val title = getString(item.value.nameId).lowercase()
val similarity = similarityAlgorithm.similarity(searchTerm, title)
if (similarity > 0.08) {
Pair(similarity, item)
} else {
null
}
}.sortedByDescending { it.first }.mapNotNull {
val item = it.second.value
val pairedSettingKey = item.setting.pairedSettingKey
val optionalSetting: SettingsItem? = if (pairedSettingKey.isNotEmpty()) {
val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false)
if (pairedSettingValue) it.second.value else null
} else {
it.second.value
}
optionalSetting
}
settingsAdapter?.submitList(sortedList)
binding.noResultsView.visibility =
if (sortedList.isEmpty()) View.VISIBLE else View.INVISIBLE
}
private fun focusSearch() {
binding.searchText.requestFocus()
val imm = requireActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
val sideMargin = resources.getDimensionPixelSize(R.dimen.spacing_medlarge)
val topMargin = resources.getDimensionPixelSize(R.dimen.spacing_chip)
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
binding.settingsList.updatePadding(bottom = barInsets.bottom + extraListSpacing)
binding.frameSearch.updatePadding(
left = leftInsets + sideMargin,
top = barInsets.top + topMargin,
right = rightInsets + sideMargin
)
binding.noResultsView.updatePadding(
left = leftInsets,
right = rightInsets,
bottom = barInsets.bottom
)
val mlpSettingsList = binding.settingsList.layoutParams as ViewGroup.MarginLayoutParams
mlpSettingsList.leftMargin = leftInsets + sideMargin
mlpSettingsList.rightMargin = rightInsets + sideMargin
binding.settingsList.layoutParams = mlpSettingsList
val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams
mlpDivider.leftMargin = leftInsets + sideMargin
mlpDivider.rightMargin = rightInsets + sideMargin
binding.divider.layoutParams = mlpDivider
windowInsets
}
companion object {
const val SEARCH_TEXT = "SearchText"
}
}

View File

@ -19,6 +19,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController import androidx.navigation.findNavController
@ -32,10 +33,13 @@ import org.yuzu.yuzu_emu.adapters.SetupAdapter
import org.yuzu.yuzu_emu.databinding.FragmentSetupBinding import org.yuzu.yuzu_emu.databinding.FragmentSetupBinding
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.SetupCallback
import org.yuzu.yuzu_emu.model.SetupPage import org.yuzu.yuzu_emu.model.SetupPage
import org.yuzu.yuzu_emu.model.StepState
import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.GameHelper import org.yuzu.yuzu_emu.utils.GameHelper
import org.yuzu.yuzu_emu.utils.ViewUtils
class SetupFragment : Fragment() { class SetupFragment : Fragment() {
private var _binding: FragmentSetupBinding? = null private var _binding: FragmentSetupBinding? = null
@ -112,14 +116,22 @@ class SetupFragment : Fragment() {
0, 0,
false, false,
R.string.give_permission, R.string.give_permission,
{ permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) }, {
notificationCallback = it
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
},
true, true,
R.string.notification_warning, R.string.notification_warning,
R.string.notification_warning_description, R.string.notification_warning_description,
0, 0,
{ {
NotificationManagerCompat.from(requireContext()) if (NotificationManagerCompat.from(requireContext())
.areNotificationsEnabled() .areNotificationsEnabled()
) {
StepState.COMPLETE
} else {
StepState.INCOMPLETE
}
} }
) )
) )
@ -133,12 +145,22 @@ class SetupFragment : Fragment() {
R.drawable.ic_add, R.drawable.ic_add,
true, true,
R.string.select_keys, R.string.select_keys,
{ mainActivity.getProdKey.launch(arrayOf("*/*")) }, {
keyCallback = it
getProdKey.launch(arrayOf("*/*"))
},
true, true,
R.string.install_prod_keys_warning, R.string.install_prod_keys_warning,
R.string.install_prod_keys_warning_description, R.string.install_prod_keys_warning_description,
R.string.install_prod_keys_warning_help, R.string.install_prod_keys_warning_help,
{ File(DirectoryInitialization.userDirectory + "/keys/prod.keys").exists() } {
val file = File(DirectoryInitialization.userDirectory + "/keys/prod.keys")
if (file.exists()) {
StepState.COMPLETE
} else {
StepState.INCOMPLETE
}
}
) )
) )
add( add(
@ -150,9 +172,8 @@ class SetupFragment : Fragment() {
true, true,
R.string.add_games, R.string.add_games,
{ {
mainActivity.getGamesDirectory.launch( gamesDirCallback = it
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
)
}, },
true, true,
R.string.add_games_warning, R.string.add_games_warning,
@ -163,7 +184,11 @@ class SetupFragment : Fragment() {
PreferenceManager.getDefaultSharedPreferences( PreferenceManager.getDefaultSharedPreferences(
YuzuApplication.appContext YuzuApplication.appContext
) )
preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty() if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) {
StepState.COMPLETE
} else {
StepState.INCOMPLETE
}
} }
) )
) )
@ -181,6 +206,13 @@ class SetupFragment : Fragment() {
) )
} }
homeViewModel.shouldPageForward.observe(viewLifecycleOwner) {
if (it) {
pageForward()
homeViewModel.setShouldPageForward(false)
}
}
binding.viewPager2.apply { binding.viewPager2.apply {
adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages) adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
offscreenPageLimit = 2 offscreenPageLimit = 2
@ -194,15 +226,15 @@ class SetupFragment : Fragment() {
super.onPageSelected(position) super.onPageSelected(position)
if (position == 1 && previousPosition == 0) { if (position == 1 && previousPosition == 0) {
showView(binding.buttonNext) ViewUtils.showView(binding.buttonNext)
showView(binding.buttonBack) ViewUtils.showView(binding.buttonBack)
} else if (position == 0 && previousPosition == 1) { } else if (position == 0 && previousPosition == 1) {
hideView(binding.buttonBack) ViewUtils.hideView(binding.buttonBack)
hideView(binding.buttonNext) ViewUtils.hideView(binding.buttonNext)
} else if (position == pages.size - 1 && previousPosition == pages.size - 2) { } else if (position == pages.size - 1 && previousPosition == pages.size - 2) {
hideView(binding.buttonNext) ViewUtils.hideView(binding.buttonNext)
} else if (position == pages.size - 2 && previousPosition == pages.size - 1) { } else if (position == pages.size - 2 && previousPosition == pages.size - 1) {
showView(binding.buttonNext) ViewUtils.showView(binding.buttonNext)
} }
previousPosition = position previousPosition = position
@ -215,7 +247,8 @@ class SetupFragment : Fragment() {
// Checks if the user has completed the task on the current page // Checks if the user has completed the task on the current page
if (currentPage.hasWarning) { if (currentPage.hasWarning) {
if (currentPage.taskCompleted.invoke()) { val stepState = currentPage.stepCompleted.invoke()
if (stepState != StepState.INCOMPLETE) {
pageForward() pageForward()
return@setOnClickListener return@setOnClickListener
} }
@ -264,9 +297,15 @@ class SetupFragment : Fragment() {
_binding = null _binding = null
} }
private lateinit var notificationCallback: SetupCallback
@RequiresApi(Build.VERSION_CODES.TIRAMISU) @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private val permissionLauncher = private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) {
notificationCallback.onStepCompleted()
}
if (!it && if (!it &&
!shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) !shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
) { ) {
@ -277,6 +316,27 @@ class SetupFragment : Fragment() {
} }
} }
private lateinit var keyCallback: SetupCallback
val getProdKey =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result != null) {
if (mainActivity.processKey(result)) {
keyCallback.onStepCompleted()
}
}
}
private lateinit var gamesDirCallback: SetupCallback
val getGamesDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) {
mainActivity.processGamesDir(result)
gamesDirCallback.onStepCompleted()
}
}
private fun finishSetup() { private fun finishSetup() {
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
.putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false) .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false)
@ -284,33 +344,6 @@ class SetupFragment : Fragment() {
mainActivity.finishSetup(binding.root.findNavController()) mainActivity.finishSetup(binding.root.findNavController())
} }
private fun showView(view: View) {
view.apply {
alpha = 0f
visibility = View.VISIBLE
isClickable = true
}.animate().apply {
duration = 300
alpha(1f)
}.start()
}
private fun hideView(view: View) {
if (view.visibility == View.INVISIBLE) {
return
}
view.apply {
alpha = 1f
isClickable = false
}.animate().apply {
duration = 300
alpha(0f)
}.withEndAction {
view.visibility = View.INVISIBLE
}
}
fun pageForward() { fun pageForward() {
binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1 binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1
} }
@ -326,15 +359,29 @@ class SetupFragment : Fragment() {
private fun setInsets() = private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener( ViewCompat.setOnApplyWindowInsetsListener(
binding.root binding.root
) { view: View, windowInsets: WindowInsetsCompat -> ) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
view.setPadding(
barInsets.left + cutoutInsets.left, val leftPadding = barInsets.left + cutoutInsets.left
barInsets.top + cutoutInsets.top, val topPadding = barInsets.top + cutoutInsets.top
barInsets.right + cutoutInsets.right, val rightPadding = barInsets.right + cutoutInsets.right
barInsets.bottom + cutoutInsets.bottom val bottomPadding = barInsets.bottom + cutoutInsets.bottom
if (resources.getBoolean(R.bool.small_layout)) {
binding.viewPager2
.updatePadding(left = leftPadding, top = topPadding, right = rightPadding)
binding.constraintButtons
.updatePadding(left = leftPadding, right = rightPadding, bottom = bottomPadding)
} else {
binding.viewPager2.updatePadding(top = topPadding, bottom = bottomPadding)
binding.constraintButtons
.updatePadding(
left = leftPadding,
right = rightPadding,
bottom = bottomPadding
) )
}
windowInsets windowInsets
} }
} }

View File

@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.model
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class EmulationViewModel : ViewModel() {
private val _emulationStarted = MutableLiveData(false)
val emulationStarted: LiveData<Boolean> get() = _emulationStarted
private val _isEmulationStopping = MutableLiveData(false)
val isEmulationStopping: LiveData<Boolean> get() = _isEmulationStopping
private val _shaderProgress = MutableLiveData(0)
val shaderProgress: LiveData<Int> get() = _shaderProgress
private val _totalShaders = MutableLiveData(0)
val totalShaders: LiveData<Int> get() = _totalShaders
private val _shaderMessage = MutableLiveData("")
val shaderMessage: LiveData<String> get() = _shaderMessage
fun setEmulationStarted(started: Boolean) {
_emulationStarted.postValue(started)
}
fun setIsEmulationStopping(value: Boolean) {
_isEmulationStopping.value = value
}
fun setShaderProgress(progress: Int) {
_shaderProgress.value = progress
}
fun setTotalShaders(max: Int) {
_totalShaders.value = max
}
fun setShaderMessage(msg: String) {
_shaderMessage.value = msg
}
fun updateProgress(msg: String, progress: Int, max: Int) {
setShaderMessage(msg)
setShaderProgress(progress)
setTotalShaders(max)
}
fun clear() {
_emulationStarted.value = false
_isEmulationStopping.value = false
_shaderProgress.value = 0
_totalShaders.value = 0
_shaderMessage.value = ""
}
}

View File

@ -3,6 +3,9 @@
package org.yuzu.yuzu_emu.model package org.yuzu.yuzu_emu.model
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
data class HomeSetting( data class HomeSetting(
val titleId: Int, val titleId: Int,
val descriptionId: Int, val descriptionId: Int,
@ -10,5 +13,6 @@ data class HomeSetting(
val onClick: () -> Unit, val onClick: () -> Unit,
val isEnabled: () -> Boolean = { true }, val isEnabled: () -> Boolean = { true },
val disabledTitleId: Int = 0, val disabledTitleId: Int = 0,
val disabledMessageId: Int = 0 val disabledMessageId: Int = 0,
val details: LiveData<String> = MutableLiveData("")
) )

View File

@ -3,9 +3,15 @@
package org.yuzu.yuzu_emu.model package org.yuzu.yuzu_emu.model
import android.net.Uri
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.utils.GameHelper
class HomeViewModel : ViewModel() { class HomeViewModel : ViewModel() {
private val _navigationVisible = MutableLiveData<Pair<Boolean, Boolean>>() private val _navigationVisible = MutableLiveData<Pair<Boolean, Boolean>>()
@ -14,6 +20,17 @@ class HomeViewModel : ViewModel() {
private val _statusBarShadeVisible = MutableLiveData(true) private val _statusBarShadeVisible = MutableLiveData(true)
val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible
private val _shouldPageForward = MutableLiveData(false)
val shouldPageForward: LiveData<Boolean> get() = _shouldPageForward
private val _gamesDir = MutableLiveData(
Uri.parse(
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
.getString(GameHelper.KEY_GAME_PATH, "")
).path ?: ""
)
val gamesDir: LiveData<String> get() = _gamesDir
var navigatedToSetup = false var navigatedToSetup = false
init { init {
@ -33,4 +50,13 @@ class HomeViewModel : ViewModel() {
} }
_statusBarShadeVisible.value = visible _statusBarShadeVisible.value = visible
} }
fun setShouldPageForward(pageForward: Boolean) {
_shouldPageForward.value = pageForward
}
fun setGamesDir(activity: FragmentActivity, dir: String) {
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
_gamesDir.value = dir
}
} }

View File

@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.model
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
class SettingsViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
var game: Game? = null
var shouldSave = false
var clickedItem: SettingsItem? = null
private val _toolbarTitle = MutableLiveData("")
val toolbarTitle: LiveData<String> get() = _toolbarTitle
private val _shouldRecreate = MutableLiveData(false)
val shouldRecreate: LiveData<Boolean> get() = _shouldRecreate
private val _shouldNavigateBack = MutableLiveData(false)
val shouldNavigateBack: LiveData<Boolean> get() = _shouldNavigateBack
private val _shouldShowResetSettingsDialog = MutableLiveData(false)
val shouldShowResetSettingsDialog: LiveData<Boolean> get() = _shouldShowResetSettingsDialog
private val _shouldReloadSettingsList = MutableLiveData(false)
val shouldReloadSettingsList: LiveData<Boolean> get() = _shouldReloadSettingsList
private val _isUsingSearch = MutableLiveData(false)
val isUsingSearch: LiveData<Boolean> get() = _isUsingSearch
val sliderProgress = savedStateHandle.getStateFlow(KEY_SLIDER_PROGRESS, -1)
val sliderTextValue = savedStateHandle.getStateFlow(KEY_SLIDER_TEXT_VALUE, "")
val adapterItemChanged = savedStateHandle.getStateFlow(KEY_ADAPTER_ITEM_CHANGED, -1)
fun setToolbarTitle(value: String) {
_toolbarTitle.value = value
}
fun setShouldRecreate(value: Boolean) {
_shouldRecreate.value = value
}
fun setShouldNavigateBack(value: Boolean) {
_shouldNavigateBack.value = value
}
fun setShouldShowResetSettingsDialog(value: Boolean) {
_shouldShowResetSettingsDialog.value = value
}
fun setShouldReloadSettingsList(value: Boolean) {
_shouldReloadSettingsList.value = value
}
fun setIsUsingSearch(value: Boolean) {
_isUsingSearch.value = value
}
fun setSliderTextValue(value: Float, units: String) {
savedStateHandle[KEY_SLIDER_PROGRESS] = value
savedStateHandle[KEY_SLIDER_TEXT_VALUE] = String.format(
YuzuApplication.appContext.getString(R.string.value_with_units),
value.toInt().toString(),
units
)
}
fun setSliderProgress(value: Float) {
savedStateHandle[KEY_SLIDER_PROGRESS] = value
}
fun setAdapterItemChanged(value: Int) {
savedStateHandle[KEY_ADAPTER_ITEM_CHANGED] = value
}
fun clear() {
game = null
shouldSave = false
}
companion object {
const val KEY_SLIDER_TEXT_VALUE = "SliderTextValue"
const val KEY_SLIDER_PROGRESS = "SliderProgress"
const val KEY_ADAPTER_ITEM_CHANGED = "AdapterItemChanged"
}
}

View File

@ -10,10 +10,20 @@ data class SetupPage(
val buttonIconId: Int, val buttonIconId: Int,
val leftAlignedIcon: Boolean, val leftAlignedIcon: Boolean,
val buttonTextId: Int, val buttonTextId: Int,
val buttonAction: () -> Unit, val buttonAction: (callback: SetupCallback) -> Unit,
val hasWarning: Boolean, val hasWarning: Boolean,
val warningTitleId: Int = 0, val warningTitleId: Int = 0,
val warningDescriptionId: Int = 0, val warningDescriptionId: Int = 0,
val warningHelpLinkId: Int = 0, val warningHelpLinkId: Int = 0,
val taskCompleted: () -> Boolean = { true } val stepCompleted: () -> StepState = { StepState.UNDEFINED }
) )
interface SetupCallback {
fun onStepCompleted()
}
enum class StepState {
COMPLETE,
INCOMPLETE,
UNDEFINED
}

View File

@ -33,17 +33,15 @@ import java.io.IOException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
import org.yuzu.yuzu_emu.fragments.LongMessageDialogFragment
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.HomeViewModel
@ -54,7 +52,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
private val homeViewModel: HomeViewModel by viewModels() private val homeViewModel: HomeViewModel by viewModels()
private val gamesViewModel: GamesViewModel by viewModels() private val gamesViewModel: GamesViewModel by viewModels()
private val settingsViewModel: SettingsViewModel by viewModels()
override var themeId: Int = 0 override var themeId: Int = 0
@ -62,8 +59,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val splashScreen = installSplashScreen() val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
settingsViewModel.settings.loadSettings()
ThemeHelper.setTheme(this) ThemeHelper.setTheme(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -109,11 +104,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
when (it.itemId) { when (it.itemId) {
R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true) R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true)
R.id.searchFragment -> gamesViewModel.setSearchFocused(true) R.id.searchFragment -> gamesViewModel.setSearchFocused(true)
R.id.homeSettingsFragment -> SettingsActivity.launch( R.id.homeSettingsFragment -> {
this, val action = HomeNavigationDirections.actionGlobalSettingsActivity(
SettingsFile.FILE_NAME_CONFIG, null,
"" SettingsFile.FILE_NAME_CONFIG
) )
navHostFragment.navController.navigate(action)
}
} }
} }
@ -266,10 +263,12 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val getGamesDirectory = val getGamesDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result == null) { if (result != null) {
return@registerForActivityResult processGamesDir(result)
}
} }
fun processGamesDir(result: Uri) {
contentResolver.takePersistableUriPermission( contentResolver.takePersistableUriPermission(
result, result,
Intent.FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION
@ -288,20 +287,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
).show() ).show()
gamesViewModel.reloadGames(true) gamesViewModel.reloadGames(true)
homeViewModel.setGamesDir(this, result.path!!)
} }
val getProdKey = val getProdKey =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null) { if (result != null) {
return@registerForActivityResult processKey(result)
}
} }
fun processKey(result: Uri): Boolean {
if (FileUtil.getExtension(result) != "keys") { if (FileUtil.getExtension(result) != "keys") {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
R.string.reading_keys_failure, titleId = R.string.reading_keys_failure,
R.string.install_prod_keys_failure_extension_description descriptionId = R.string.install_prod_keys_failure_extension_description
).show(supportFragmentManager, MessageDialogFragment.TAG) ).show(supportFragmentManager, MessageDialogFragment.TAG)
return@registerForActivityResult return false
} }
contentResolver.takePersistableUriPermission( contentResolver.takePersistableUriPermission(
@ -324,14 +326,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
gamesViewModel.reloadGames(true) gamesViewModel.reloadGames(true)
return true
} else { } else {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
R.string.invalid_keys_error, titleId = R.string.invalid_keys_error,
R.string.install_keys_failure_description, descriptionId = R.string.install_keys_failure_description,
R.string.dumping_keys_quickstart_link helpLinkId = R.string.dumping_keys_quickstart_link
).show(supportFragmentManager, MessageDialogFragment.TAG) ).show(supportFragmentManager, MessageDialogFragment.TAG)
return false
} }
} }
return false
} }
val getFirmware = val getFirmware =
@ -364,8 +369,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
R.string.firmware_installed_failure, titleId = R.string.firmware_installed_failure,
R.string.firmware_installed_failure_description descriptionId = R.string.firmware_installed_failure_description
) )
} else { } else {
firmwarePath.deleteRecursively() firmwarePath.deleteRecursively()
@ -395,8 +400,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
if (FileUtil.getExtension(result) != "bin") { if (FileUtil.getExtension(result) != "bin") {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
R.string.reading_keys_failure, titleId = R.string.reading_keys_failure,
R.string.install_amiibo_keys_failure_extension_description descriptionId = R.string.install_amiibo_keys_failure_extension_description
).show(supportFragmentManager, MessageDialogFragment.TAG) ).show(supportFragmentManager, MessageDialogFragment.TAG)
return@registerForActivityResult return@registerForActivityResult
} }
@ -422,9 +427,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
).show() ).show()
} else { } else {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
R.string.invalid_keys_error, titleId = R.string.invalid_keys_error,
R.string.install_keys_failure_description, descriptionId = R.string.install_keys_failure_description,
R.string.dumping_keys_quickstart_link helpLinkId = R.string.dumping_keys_quickstart_link
).show(supportFragmentManager, MessageDialogFragment.TAG) ).show(supportFragmentManager, MessageDialogFragment.TAG)
} }
} }
@ -496,8 +501,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
var errorBaseGame = 0 var errorBaseGame = 0
var errorExtension = 0 var errorExtension = 0
var errorOther = 0 var errorOther = 0
var errorTotal = 0
lifecycleScope.launch {
documents.forEach { documents.forEach {
when (NativeLibrary.installFileToNand(it.toString())) { when (NativeLibrary.installFileToNand(it.toString())) {
NativeLibrary.InstallFileToNandResult.Success -> { NativeLibrary.InstallFileToNandResult.Success -> {
@ -521,7 +524,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
} }
} }
} }
withContext(Dispatchers.Main) {
val separator = System.getProperty("line.separator") ?: "\n" val separator = System.getProperty("line.separator") ?: "\n"
val installResult = StringBuilder() val installResult = StringBuilder()
if (installSuccess > 0) { if (installSuccess > 0) {
@ -542,7 +545,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
) )
installResult.append(separator) installResult.append(separator)
} }
errorTotal = errorBaseGame + errorExtension + errorOther val errorTotal: Int = errorBaseGame + errorExtension + errorOther
if (errorTotal > 0) { if (errorTotal > 0) {
installResult.append(separator) installResult.append(separator)
installResult.append( installResult.append(
@ -572,20 +575,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
) )
installResult.append(separator) installResult.append(separator)
} }
LongMessageDialogFragment.newInstance( return@newInstance MessageDialogFragment.newInstance(
R.string.install_game_content_failure, titleId = R.string.install_game_content_failure,
installResult.toString().trim(), descriptionString = installResult.toString().trim(),
R.string.install_game_content_help_link helpLinkId = R.string.install_game_content_help_link
).show(supportFragmentManager, LongMessageDialogFragment.TAG) )
} else { } else {
LongMessageDialogFragment.newInstance( return@newInstance MessageDialogFragment.newInstance(
R.string.install_game_content_success, titleId = R.string.install_game_content_success,
installResult.toString().trim() descriptionString = installResult.toString().trim()
).show(supportFragmentManager, LongMessageDialogFragment.TAG) )
} }
}
}
return@newInstance installSuccess + installOverwrite + errorTotal
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
} }

View File

@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
class BiMap<K, V> {
private val forward: MutableMap<K, V> = HashMap()
private val backward: MutableMap<V, K> = HashMap()
@Synchronized
fun add(key: K, value: V) {
forward[key] = value
backward[value] = key
}
@Synchronized
fun getForward(key: K): V? {
return forward[key]
}
@Synchronized
fun getBackward(key: V): K? {
return backward[key]
}
}

View File

@ -3,18 +3,18 @@
package org.yuzu.yuzu_emu.utils package org.yuzu.yuzu_emu.utils
import android.content.Context
import java.io.IOException import java.io.IOException
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.YuzuApplication
object DirectoryInitialization { object DirectoryInitialization {
private var userPath: String? = null private var userPath: String? = null
var areDirectoriesReady: Boolean = false var areDirectoriesReady: Boolean = false
fun start(context: Context) { fun start() {
if (!areDirectoriesReady) { if (!areDirectoriesReady) {
initializeInternalStorage(context) initializeInternalStorage()
NativeLibrary.initializeEmulation() NativeLibrary.initializeEmulation()
areDirectoriesReady = true areDirectoriesReady = true
} }
@ -26,9 +26,9 @@ object DirectoryInitialization {
return userPath return userPath
} }
private fun initializeInternalStorage(context: Context) { private fun initializeInternalStorage() {
try { try {
userPath = context.getExternalFilesDir(null)!!.canonicalPath userPath = YuzuApplication.appContext.getExternalFilesDir(null)!!.canonicalPath
NativeLibrary.setAppDirectory(userPath!!) NativeLibrary.setAppDirectory(userPath!!)
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() e.printStackTrace()

View File

@ -11,6 +11,7 @@ import kotlinx.serialization.json.Json
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
object GameHelper { object GameHelper {
const val KEY_GAME_PATH = "game_path" const val KEY_GAME_PATH = "game_path"
@ -29,15 +30,7 @@ object GameHelper {
// Ensure keys are loaded so that ROM metadata can be decrypted. // Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys() NativeLibrary.reloadKeys()
val children = FileUtil.listFiles(context, gamesUri) addGamesRecursive(games, FileUtil.listFiles(context, gamesUri), 3)
for (file in children) {
if (!file.isDirectory) {
// Check that the file has an extension we care about before trying to read out of it.
if (Game.extensions.contains(FileUtil.getExtension(file.uri))) {
games.add(getGame(file.uri))
}
}
}
// Cache list of games found on disk // Cache list of games found on disk
val serializedGames = mutableSetOf<String>() val serializedGames = mutableSetOf<String>()
@ -52,7 +45,31 @@ object GameHelper {
return games.toList() return games.toList()
} }
private fun getGame(uri: Uri): Game { private fun addGamesRecursive(
games: MutableList<Game>,
files: Array<MinimalDocumentFile>,
depth: Int
) {
if (depth <= 0) {
return
}
files.forEach {
if (it.isDirectory) {
addGamesRecursive(
games,
FileUtil.listFiles(YuzuApplication.appContext, it.uri),
depth - 1
)
} else {
if (Game.extensions.contains(FileUtil.getExtension(it.uri))) {
games.add(getGame(it.uri, true))
}
}
}
}
fun getGame(uri: Uri, addedToLibrary: Boolean): Game {
val filePath = uri.toString() val filePath = uri.toString()
var name = NativeLibrary.getTitle(filePath) var name = NativeLibrary.getTitle(filePath)
@ -77,12 +94,14 @@ object GameHelper {
NativeLibrary.isHomebrew(filePath) NativeLibrary.isHomebrew(filePath)
) )
if (addedToLibrary) {
val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L) val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L)
if (addedTime == 0L) { if (addedTime == 0L) {
preferences.edit() preferences.edit()
.putLong(newGame.keyAddedToLibraryTime, System.currentTimeMillis()) .putLong(newGame.keyAddedToLibraryTime, System.currentTimeMillis())
.apply() .apply()
} }
}
return newGame return newGame
} }

View File

@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.widget.ImageView
import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader
import coil.decode.DataSource
import coil.fetch.DrawableResult
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.key.Keyer
import coil.memory.MemoryCache
import coil.request.ImageRequest
import coil.request.Options
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.Game
class GameIconFetcher(
private val game: Game,
private val options: Options
) : Fetcher {
override suspend fun fetch(): FetchResult {
return DrawableResult(
drawable = decodeGameIcon(game.path)!!.toDrawable(options.context.resources),
isSampled = false,
dataSource = DataSource.DISK
)
}
private fun decodeGameIcon(uri: String): Bitmap? {
val data = NativeLibrary.getIcon(uri)
return BitmapFactory.decodeByteArray(
data,
0,
data.size,
BitmapFactory.Options()
)
}
class Factory : Fetcher.Factory<Game> {
override fun create(data: Game, options: Options, imageLoader: ImageLoader): Fetcher =
GameIconFetcher(data, options)
}
}
class GameIconKeyer : Keyer<Game> {
override fun key(data: Game, options: Options): String = data.path
}
object GameIconUtils {
private val imageLoader = ImageLoader.Builder(YuzuApplication.appContext)
.components {
add(GameIconKeyer())
add(GameIconFetcher.Factory())
}
.memoryCache {
MemoryCache.Builder(YuzuApplication.appContext)
.maxSizePercent(0.25)
.build()
}
.build()
fun loadGameIcon(game: Game, imageView: ImageView) {
val request = ImageRequest.Builder(YuzuApplication.appContext)
.data(game)
.target(imageView)
.error(R.drawable.default_icon)
.build()
imageLoader.enqueue(request)
}
}

View File

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
object NativeConfig {
external fun getBoolean(key: String, getDefault: Boolean): Boolean
external fun setBoolean(key: String, value: Boolean)
external fun getByte(key: String, getDefault: Boolean): Byte
external fun setByte(key: String, value: Byte)
external fun getShort(key: String, getDefault: Boolean): Short
external fun setShort(key: String, value: Short)
external fun getInt(key: String, getDefault: Boolean): Int
external fun setInt(key: String, value: Int)
external fun getFloat(key: String, getDefault: Boolean): Float
external fun setFloat(key: String, value: Float)
external fun getLong(key: String, getDefault: Boolean): Long
external fun setLong(key: String, value: Long)
external fun getString(key: String, getDefault: Boolean): String
external fun setString(key: String, value: String)
external fun getIsRuntimeModifiable(key: String): Boolean
external fun getConfigHeader(category: Int): String
external fun getPairedSettingKey(key: String): String
}

View File

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
import android.view.View
object ViewUtils {
fun showView(view: View, length: Long = 300) {
view.apply {
alpha = 0f
visibility = View.VISIBLE
isClickable = true
}.animate().apply {
duration = length
alpha(1f)
}.start()
}
fun hideView(view: View, length: Long = 300) {
if (view.visibility == View.INVISIBLE) {
return
}
view.apply {
alpha = 1f
isClickable = false
}.animate().apply {
duration = length
alpha(0f)
}.withEndAction {
view.visibility = View.INVISIBLE
}.start()
}
}

View File

@ -7,7 +7,6 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Rational import android.util.Rational
import android.view.SurfaceView import android.view.SurfaceView
import kotlin.math.roundToInt
class FixedRatioSurfaceView @JvmOverloads constructor( class FixedRatioSurfaceView @JvmOverloads constructor(
context: Context, context: Context,
@ -22,27 +21,44 @@ class FixedRatioSurfaceView @JvmOverloads constructor(
*/ */
fun setAspectRatio(ratio: Rational?) { fun setAspectRatio(ratio: Rational?) {
aspectRatio = ratio?.toFloat() ?: 0f aspectRatio = ratio?.toFloat() ?: 0f
requestLayout()
} }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec) val displayWidth: Float = MeasureSpec.getSize(widthMeasureSpec).toFloat()
val width = MeasureSpec.getSize(widthMeasureSpec) val displayHeight: Float = MeasureSpec.getSize(heightMeasureSpec).toFloat()
val height = MeasureSpec.getSize(heightMeasureSpec)
if (aspectRatio != 0f) { if (aspectRatio != 0f) {
val newWidth: Int val displayAspect = displayWidth / displayHeight
val newHeight: Int if (displayAspect < aspectRatio) {
if (height * aspectRatio < width) { // Max out width
newWidth = (height * aspectRatio).roundToInt() val halfHeight = displayHeight / 2
newHeight = height val surfaceHeight = displayWidth / aspectRatio
val newTop: Float = halfHeight - (surfaceHeight / 2)
val newBottom: Float = halfHeight + (surfaceHeight / 2)
super.onMeasure(
widthMeasureSpec,
MeasureSpec.makeMeasureSpec(
newBottom.toInt() - newTop.toInt(),
MeasureSpec.EXACTLY
)
)
return
} else { } else {
newWidth = width // Max out height
newHeight = (width / aspectRatio).roundToInt() val halfWidth = displayWidth / 2
} val surfaceWidth = displayHeight * aspectRatio
val left = (width - newWidth) / 2 val newLeft: Float = halfWidth - (surfaceWidth / 2)
val top = (height - newHeight) / 2 val newRight: Float = halfWidth + (surfaceWidth / 2)
setLeftTopRightBottom(left, top, left + newWidth, top + newHeight) super.onMeasure(
} else { MeasureSpec.makeMeasureSpec(
setLeftTopRightBottom(0, 0, width, height) newRight.toInt() - newLeft.toInt(),
MeasureSpec.EXACTLY
),
heightMeasureSpec
)
return
} }
} }
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
} }

View File

@ -14,6 +14,8 @@ add_library(yuzu-android SHARED
id_cache.cpp id_cache.cpp
id_cache.h id_cache.h
native.cpp native.cpp
native_config.cpp
uisettings.cpp
) )
set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR}) set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})

View File

@ -11,22 +11,25 @@
#include "common/fs/path_util.h" #include "common/fs/path_util.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "common/settings.h" #include "common/settings.h"
#include "common/settings_enums.h"
#include "core/hle/service/acc/profile_manager.h" #include "core/hle/service/acc/profile_manager.h"
#include "input_common/main.h" #include "input_common/main.h"
#include "jni/config.h" #include "jni/config.h"
#include "jni/default_ini.h" #include "jni/default_ini.h"
#include "uisettings.h"
namespace FS = Common::FS; namespace FS = Common::FS;
Config::Config(std::optional<std::filesystem::path> config_path) Config::Config(const std::string& config_name, ConfigType config_type)
: config_loc{config_path.value_or(FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "config.ini")}, : type(config_type), global{config_type == ConfigType::GlobalConfig} {
config{std::make_unique<INIReader>(FS::PathToUTF8String(config_loc))} { Initialize(config_name);
Reload();
} }
Config::~Config() = default; Config::~Config() = default;
bool Config::LoadINI(const std::string& default_contents, bool retry) { bool Config::LoadINI(const std::string& default_contents, bool retry) {
void(FS::CreateParentDir(config_loc));
config = std::make_unique<INIReader>(FS::PathToUTF8String(config_loc));
const auto config_loc_str = FS::PathToUTF8String(config_loc); const auto config_loc_str = FS::PathToUTF8String(config_loc);
if (config->ParseError() < 0) { if (config->ParseError() < 0) {
if (retry) { if (retry) {
@ -144,7 +147,9 @@ void Config::ReadValues() {
Service::Account::MAX_USERS - 1); Service::Account::MAX_USERS - 1);
// Disable docked mode by default on Android // Disable docked mode by default on Android
Settings::values.use_docked_mode = config->GetBoolean("System", "use_docked_mode", false); Settings::values.use_docked_mode.SetValue(config->GetBoolean("System", "use_docked_mode", false)
? Settings::ConsoleMode::Docked
: Settings::ConsoleMode::Handheld);
const auto rng_seed_enabled = config->GetBoolean("System", "rng_seed_enabled", false); const auto rng_seed_enabled = config->GetBoolean("System", "rng_seed_enabled", false);
if (rng_seed_enabled) { if (rng_seed_enabled) {
@ -298,9 +303,28 @@ void Config::ReadValues() {
// Network // Network
ReadSetting("Network", Settings::values.network_interface); ReadSetting("Network", Settings::values.network_interface);
// Android
ReadSetting("Android", AndroidSettings::values.picture_in_picture);
ReadSetting("Android", AndroidSettings::values.screen_layout);
} }
void Config::Reload() { void Config::Initialize(const std::string& config_name) {
const auto fs_config_loc = FS::GetYuzuPath(FS::YuzuPath::ConfigDir);
const auto config_file = fmt::format("{}.ini", config_name);
switch (type) {
case ConfigType::GlobalConfig:
config_loc = FS::PathToUTF8String(fs_config_loc / config_file);
break;
case ConfigType::PerGameConfig:
config_loc = FS::PathToUTF8String(fs_config_loc / "custom" / FS::ToU8String(config_file));
break;
case ConfigType::InputProfile:
config_loc = FS::PathToUTF8String(fs_config_loc / "input" / config_file);
LoadINI(DefaultINI::android_config_file);
return;
}
LoadINI(DefaultINI::android_config_file); LoadINI(DefaultINI::android_config_file);
ReadValues(); ReadValues();
} }

View File

@ -13,25 +13,35 @@
class INIReader; class INIReader;
class Config { class Config {
std::filesystem::path config_loc;
std::unique_ptr<INIReader> config;
bool LoadINI(const std::string& default_contents = "", bool retry = true); bool LoadINI(const std::string& default_contents = "", bool retry = true);
void ReadValues();
public: public:
explicit Config(std::optional<std::filesystem::path> config_path = std::nullopt); enum class ConfigType {
GlobalConfig,
PerGameConfig,
InputProfile,
};
explicit Config(const std::string& config_name = "config",
ConfigType config_type = ConfigType::GlobalConfig);
~Config(); ~Config();
void Reload(); void Initialize(const std::string& config_name);
private: private:
/** /**
* Applies a value read from the sdl2_config to a Setting. * Applies a value read from the config to a Setting.
* *
* @param group The name of the INI group * @param group The name of the INI group
* @param setting The yuzu setting to modify * @param setting The yuzu setting to modify
*/ */
template <typename Type, bool ranged> template <typename Type, bool ranged>
void ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting); void ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting);
void ReadValues();
const ConfigType type;
std::unique_ptr<INIReader> config;
std::string config_loc;
const bool global;
}; };

View File

@ -15,6 +15,8 @@ static jclass s_disk_cache_progress_class;
static jclass s_load_callback_stage_class; static jclass s_load_callback_stage_class;
static jmethodID s_exit_emulation_activity; static jmethodID s_exit_emulation_activity;
static jmethodID s_disk_cache_load_progress; static jmethodID s_disk_cache_load_progress;
static jmethodID s_on_emulation_started;
static jmethodID s_on_emulation_stopped;
static constexpr jint JNI_VERSION = JNI_VERSION_1_6; static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
@ -59,6 +61,14 @@ jmethodID GetDiskCacheLoadProgress() {
return s_disk_cache_load_progress; return s_disk_cache_load_progress;
} }
jmethodID GetOnEmulationStarted() {
return s_on_emulation_started;
}
jmethodID GetOnEmulationStopped() {
return s_on_emulation_stopped;
}
} // namespace IDCache } // namespace IDCache
#ifdef __cplusplus #ifdef __cplusplus
@ -85,6 +95,10 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V"); env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
s_disk_cache_load_progress = s_disk_cache_load_progress =
env->GetStaticMethodID(s_disk_cache_progress_class, "loadProgress", "(III)V"); env->GetStaticMethodID(s_disk_cache_progress_class, "loadProgress", "(III)V");
s_on_emulation_started =
env->GetStaticMethodID(s_native_library_class, "onEmulationStarted", "()V");
s_on_emulation_stopped =
env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V");
// Initialize Android Storage // Initialize Android Storage
Common::FS::Android::RegisterCallbacks(env, s_native_library_class); Common::FS::Android::RegisterCallbacks(env, s_native_library_class);

View File

@ -15,5 +15,7 @@ jclass GetDiskCacheProgressClass();
jclass GetDiskCacheLoadCallbackStageClass(); jclass GetDiskCacheLoadCallbackStageClass();
jmethodID GetExitEmulationActivity(); jmethodID GetExitEmulationActivity();
jmethodID GetDiskCacheLoadProgress(); jmethodID GetDiskCacheLoadProgress();
jmethodID GetOnEmulationStarted();
jmethodID GetOnEmulationStopped();
} // namespace IDCache } // namespace IDCache

View File

@ -30,6 +30,7 @@
#include "core/cpu_manager.h" #include "core/cpu_manager.h"
#include "core/crypto/key_manager.h" #include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.h" #include "core/file_sys/card_image.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/registered_cache.h" #include "core/file_sys/registered_cache.h"
#include "core/file_sys/submission_package.h" #include "core/file_sys/submission_package.h"
#include "core/file_sys/vfs.h" #include "core/file_sys/vfs.h"
@ -202,12 +203,10 @@ public:
} }
bool IsRunning() const { bool IsRunning() const {
std::scoped_lock lock(m_mutex);
return m_is_running; return m_is_running;
} }
bool IsPaused() const { bool IsPaused() const {
std::scoped_lock lock(m_mutex);
return m_is_running && m_is_paused; return m_is_running && m_is_paused;
} }
@ -224,6 +223,42 @@ public:
m_system.Renderer().NotifySurfaceChanged(); m_system.Renderer().NotifySurfaceChanged();
} }
void ConfigureFilesystemProvider(const std::string& filepath) {
const auto file = m_system.GetFilesystem()->OpenFile(filepath, FileSys::Mode::Read);
if (!file) {
return;
}
auto loader = Loader::GetLoader(m_system, file);
if (!loader) {
return;
}
const auto file_type = loader->GetFileType();
if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
return;
}
u64 program_id = 0;
const auto res2 = loader->ReadProgramId(program_id);
if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
m_manual_provider->AddEntry(FileSys::TitleType::Application,
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
program_id, file);
} else if (res2 == Loader::ResultStatus::Success &&
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
const auto nsp = file_type == Loader::FileType::NSP
? std::make_shared<FileSys::NSP>(file)
: FileSys::XCI{file}.GetSecurePartitionNSP();
for (const auto& title : nsp->GetNCAs()) {
for (const auto& entry : title.second) {
m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
entry.second->GetBaseFile());
}
}
}
}
Core::SystemResultStatus InitializeEmulation(const std::string& filepath) { Core::SystemResultStatus InitializeEmulation(const std::string& filepath) {
std::scoped_lock lock(m_mutex); std::scoped_lock lock(m_mutex);
@ -254,8 +289,14 @@ public:
std::move(android_keyboard), // Software Keyboard std::move(android_keyboard), // Software Keyboard
nullptr, // Web Browser nullptr, // Web Browser
}); });
// Initialize filesystem.
m_manual_provider = std::make_unique<FileSys::ManualContentProvider>();
m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>()); m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
m_system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual,
m_manual_provider.get());
m_system.GetFileSystemController().CreateFactories(*m_vfs); m_system.GetFileSystemController().CreateFactories(*m_vfs);
ConfigureFilesystemProvider(filepath);
// Initialize account manager // Initialize account manager
m_profile_manager = std::make_unique<Service::Account::ProfileManager>(); m_profile_manager = std::make_unique<Service::Account::ProfileManager>();
@ -292,6 +333,8 @@ public:
// Tear down the render window. // Tear down the render window.
m_window.reset(); m_window.reset();
OnEmulationStopped(m_load_result);
} }
void PauseEmulation() { void PauseEmulation() {
@ -333,6 +376,8 @@ public:
m_system.InitializeDebugger(); m_system.InitializeDebugger();
} }
OnEmulationStarted();
while (true) { while (true) {
{ {
[[maybe_unused]] std::unique_lock lock(m_mutex); [[maybe_unused]] std::unique_lock lock(m_mutex);
@ -377,7 +422,7 @@ public:
return false; return false;
} }
return !Settings::values.use_docked_mode.GetValue(); return !Settings::IsDockedMode();
} }
void SetDeviceType([[maybe_unused]] int index, int type) { void SetDeviceType([[maybe_unused]] int index, int type) {
@ -468,6 +513,18 @@ private:
static_cast<jint>(progress), static_cast<jint>(max)); static_cast<jint>(progress), static_cast<jint>(max));
} }
static void OnEmulationStarted() {
JNIEnv* env = IDCache::GetEnvForThread();
env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
IDCache::GetOnEmulationStarted());
}
static void OnEmulationStopped(Core::SystemResultStatus result) {
JNIEnv* env = IDCache::GetEnvForThread();
env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
IDCache::GetOnEmulationStopped(), static_cast<jint>(result));
}
private: private:
static EmulationSession s_instance; static EmulationSession s_instance;
@ -485,10 +542,11 @@ private:
Core::PerfStatsResults m_perf_stats{}; Core::PerfStatsResults m_perf_stats{};
std::shared_ptr<FileSys::VfsFilesystem> m_vfs; std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
bool m_is_running{}; std::atomic<bool> m_is_running = false;
bool m_is_paused{}; std::atomic<bool> m_is_paused = false;
SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{}; SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
std::unique_ptr<Service::Account::ProfileManager> m_profile_manager; std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider;
// GPU driver parameters // GPU driver parameters
std::shared_ptr<Common::DynamicLibrary> m_vulkan_library; std::shared_ptr<Common::DynamicLibrary> m_vulkan_library;
@ -780,34 +838,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadSettings(JNIEnv* env, jclass cl
Config{}; Config{};
} }
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserSetting(JNIEnv* env, jclass clazz,
jstring j_game_id, jstring j_section,
jstring j_key) {
std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);
std::string_view section = env->GetStringUTFChars(j_section, 0);
std::string_view key = env->GetStringUTFChars(j_key, 0);
env->ReleaseStringUTFChars(j_game_id, game_id.data());
env->ReleaseStringUTFChars(j_section, section.data());
env->ReleaseStringUTFChars(j_key, key.data());
return env->NewStringUTF("");
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setUserSetting(JNIEnv* env, jclass clazz,
jstring j_game_id, jstring j_section,
jstring j_key, jstring j_value) {
std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);
std::string_view section = env->GetStringUTFChars(j_section, 0);
std::string_view key = env->GetStringUTFChars(j_key, 0);
std::string_view value = env->GetStringUTFChars(j_value, 0);
env->ReleaseStringUTFChars(j_game_id, game_id.data());
env->ReleaseStringUTFChars(j_section, section.data());
env->ReleaseStringUTFChars(j_key, key.data());
env->ReleaseStringUTFChars(j_value, value.data());
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_initGameIni(JNIEnv* env, jclass clazz, void Java_org_yuzu_yuzu_1emu_NativeLibrary_initGameIni(JNIEnv* env, jclass clazz,
jstring j_game_id) { jstring j_game_id) {
std::string_view game_id = env->GetStringUTFChars(j_game_id, 0); std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);

View File

@ -0,0 +1,237 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string>
#include <jni.h>
#include "common/logging/log.h"
#include "common/settings.h"
#include "jni/android_common/android_common.h"
#include "jni/config.h"
#include "uisettings.h"
template <typename T>
Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
auto key = GetJString(env, jkey);
auto basicSetting = Settings::values.linkage.by_key[key];
auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key];
if (basicSetting != 0) {
return static_cast<Settings::Setting<T>*>(basicSetting);
}
if (basicAndroidSetting != 0) {
return static_cast<Settings::Setting<T>*>(basicAndroidSetting);
}
LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key);
return nullptr;
}
extern "C" {
jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj,
jstring jkey, jboolean getDefault) {
auto setting = getSetting<bool>(env, jkey);
if (setting == nullptr) {
return false;
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject obj, jstring jkey,
jboolean value) {
auto setting = getSetting<bool>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(static_cast<bool>(value));
}
jbyte Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getByte(JNIEnv* env, jobject obj, jstring jkey,
jboolean getDefault) {
auto setting = getSetting<u8>(env, jkey);
if (setting == nullptr) {
return -1;
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj, jstring jkey,
jbyte value) {
auto setting = getSetting<u8>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(value);
}
jshort Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getShort(JNIEnv* env, jobject obj, jstring jkey,
jboolean getDefault) {
auto setting = getSetting<u16>(env, jkey);
if (setting == nullptr) {
return -1;
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject obj, jstring jkey,
jshort value) {
auto setting = getSetting<u16>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(value);
}
jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInt(JNIEnv* env, jobject obj, jstring jkey,
jboolean getDefault) {
auto setting = getSetting<int>(env, jkey);
if (setting == nullptr) {
return -1;
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj, jstring jkey,
jint value) {
auto setting = getSetting<int>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(value);
}
jfloat Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getFloat(JNIEnv* env, jobject obj, jstring jkey,
jboolean getDefault) {
auto setting = getSetting<float>(env, jkey);
if (setting == nullptr) {
return -1;
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject obj, jstring jkey,
jfloat value) {
auto setting = getSetting<float>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(value);
}
jlong Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getLong(JNIEnv* env, jobject obj, jstring jkey,
jboolean getDefault) {
auto setting = getSetting<long>(env, jkey);
if (setting == nullptr) {
return -1;
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj, jstring jkey,
jlong value) {
auto setting = getSetting<long>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(value);
}
jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getString(JNIEnv* env, jobject obj, jstring jkey,
jboolean getDefault) {
auto setting = getSetting<std::string>(env, jkey);
if (setting == nullptr) {
return ToJString(env, "");
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return ToJString(env, setting->GetDefault());
}
return ToJString(env, setting->GetValue());
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject obj, jstring jkey,
jstring value) {
auto setting = getSetting<std::string>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(GetJString(env, value));
}
jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj,
jstring jkey) {
auto key = GetJString(env, jkey);
auto setting = Settings::values.linkage.by_key[key];
if (setting != 0) {
return setting->RuntimeModfiable();
}
LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key);
return true;
}
jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getConfigHeader(JNIEnv* env, jobject obj,
jint jcategory) {
auto category = static_cast<Settings::Category>(jcategory);
return ToJString(env, Settings::TranslateCategory(category));
}
jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* env, jobject obj,
jstring jkey) {
auto setting = getSetting<std::string>(env, jkey);
if (setting == nullptr) {
return ToJString(env, "");
}
if (setting->PairedSetting() == nullptr) {
return ToJString(env, "");
}
return ToJString(env, setting->PairedSetting()->GetLabel());
}
} // extern "C"

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "uisettings.h"
namespace AndroidSettings {
Values values;
} // namespace AndroidSettings

View File

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <common/settings_common.h>
#include "common/common_types.h"
#include "common/settings_setting.h"
namespace AndroidSettings {
struct Values {
Settings::Linkage linkage;
// Android
Settings::Setting<bool> picture_in_picture{linkage, true, "picture_in_picture",
Settings::Category::Android};
Settings::Setting<s32> screen_layout{linkage,
5,
"screen_layout",
Settings::Category::Android,
Settings::Specialization::Default,
true,
true};
};
extern Values values;
} // namespace AndroidSettings

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="125"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1"
android:toAlpha="0" />
<translate
android:duration="125"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="0"
android:toXDelta="-75" />
</set>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="0"
android:toAlpha="1" />
<translate
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="-200"
android:toXDelta="0" />
</set>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="125"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1"
android:toAlpha="0" />
<translate
android:duration="125"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="0"
android:toXDelta="75" />
</set>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="0"
android:toAlpha="1" />
<translate
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="200"
android:toXDelta="0" />
</set>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1"
android:toAlpha="0" />
</set>

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:propertyName="translationX"
android:valueType="floatType"
android:valueFrom="-1280dp"
android:valueTo="0"
android:interpolator="@android:interpolator/decelerate_quad"
android:duration="300"/>
<objectAnimator
android:propertyName="alpha"
android:valueType="floatType"
android:valueFrom="0"
android:valueTo="1"
android:interpolator="@android:interpolator/accelerate_quad"
android:duration="300"/>
</set>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- This animation is used ONLY when a submenu is replaced. -->
<objectAnimator
android:propertyName="translationX"
android:valueType="floatType"
android:valueFrom="0"
android:valueTo="-1280dp"
android:interpolator="@android:interpolator/decelerate_quad"
android:duration="200"/>
<objectAnimator
android:propertyName="alpha"
android:valueType="floatType"
android:valueFrom="1"
android:valueTo="0"
android:interpolator="@android:interpolator/decelerate_quad"
android:duration="200"/>
</set>

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