diff --git a/src/LibRyujinx/Android/AndroidLogTarget.cs b/src/LibRyujinx/Android/AndroidLogTarget.cs index 109886f82..c32cc261a 100644 --- a/src/LibRyujinx/Android/AndroidLogTarget.cs +++ b/src/LibRyujinx/Android/AndroidLogTarget.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Logging; +using Ryujinx.Common.Logging; using Ryujinx.Common.Logging.Formatters; using Ryujinx.Common.Logging.Targets; using System; @@ -12,7 +12,7 @@ namespace LibRyujinx string ILogTarget.Name { get => _name; } - public AndroidLogTarget( string name) + public AndroidLogTarget(string name) { _name = name; _formatter = new DefaultLogFormatter(); diff --git a/src/LibRyujinx/Android/AndroidUiHandler.cs b/src/LibRyujinx/Android/AndroidUiHandler.cs new file mode 100644 index 000000000..945feb0f4 --- /dev/null +++ b/src/LibRyujinx/Android/AndroidUiHandler.cs @@ -0,0 +1,136 @@ +using LibHac.Tools.Fs; +using Ryujinx.Common.Logging; +using Ryujinx.HLE; +using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; +using Ryujinx.HLE.Ui; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LibRyujinx.Android +{ + internal class AndroidUiHandler : IHostUiHandler, IDisposable + { + public IHostUiTheme HostUiTheme => throw new NotImplementedException(); + + public ManualResetEvent _waitEvent; + public ManualResetEvent _responseEvent; + private bool _isDisposed; + private bool _isOkPressed; + private long _input; + + public AndroidUiHandler() + { + _waitEvent = new ManualResetEvent(false); + _responseEvent = new ManualResetEvent(false); + } + + public IDynamicTextInputHandler CreateDynamicTextInputHandler() + { + throw new NotImplementedException(); + } + + public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText) + { + LibRyujinx.setUiHandlerTitle(LibRyujinx.storeString(title ?? "")); + LibRyujinx.setUiHandlerMessage(LibRyujinx.storeString(message ?? "")); + LibRyujinx.setUiHandlerType(1); + + _responseEvent.Reset(); + Set(); + _responseEvent.WaitOne(); + + return _isOkPressed; + } + + public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText) + { + LibRyujinx.setUiHandlerTitle(LibRyujinx.storeString("Software Keyboard")); + LibRyujinx.setUiHandlerMessage(LibRyujinx.storeString(args.HeaderText ?? "")); + LibRyujinx.setUiHandlerWatermark(LibRyujinx.storeString(args.GuideText ?? "")); + LibRyujinx.setUiHandlerSubtitle(LibRyujinx.storeString(args.SubtitleText ?? "")); + LibRyujinx.setUiHandlerInitialText(LibRyujinx.storeString(args.InitialText ?? "")); + LibRyujinx.setUiHandlerMinLength(args.StringLengthMin); + LibRyujinx.setUiHandlerMaxLength(args.StringLengthMax); + LibRyujinx.setUiHandlerType(2); + LibRyujinx.setUiHandlerKeyboardMode((int)args.KeyboardMode); + + _responseEvent.Reset(); + Set(); + _responseEvent.WaitOne(); + + userText = _input != -1 ? LibRyujinx.GetStoredString(_input) : ""; + + return _isOkPressed; + } + + public bool DisplayMessageDialog(string title, string message) + { + LibRyujinx.setUiHandlerTitle(LibRyujinx.storeString(title ?? "")); + LibRyujinx.setUiHandlerMessage(LibRyujinx.storeString(message ?? "")); + LibRyujinx.setUiHandlerType(1); + + _responseEvent.Reset(); + Set(); + _responseEvent.WaitOne(); + + return _isOkPressed; + } + + public bool DisplayMessageDialog(ControllerAppletUiArgs args) + { + string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}"; + + string message = $"Application requests **{playerCount}** player(s) with:\n\n" + + $"**TYPES:** {args.SupportedStyles}\n\n" + + $"**PLAYERS:** {string.Join(", ", args.SupportedPlayers)}\n\n" + + (args.IsDocked ? "Docked mode set. `Handheld` is also invalid.\n\n" : "") + + "_Please reconfigure Input now and then press OK._"; + + return DisplayMessageDialog("Controller Applet", message); + } + + public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value) + { + // throw new NotImplementedException(); + } + + internal void Wait() + { + if (_isDisposed) + return; + _waitEvent.Reset(); + _waitEvent.WaitOne(); + _waitEvent.Reset(); + } + + internal void Set() + { + if (_isDisposed) + return; + _waitEvent.Set(); + } + + internal void SetResponse(bool isOkPressed, long input) + { + if (_isDisposed) + return; + _isOkPressed = isOkPressed; + _input = input; + _responseEvent.Set(); + } + + public void Dispose() + { + _isDisposed = true; + _waitEvent.Set(); + _waitEvent.Set(); + _responseEvent.Dispose(); + _waitEvent.Dispose(); + } + } +} diff --git a/src/LibRyujinx/Android/JniExportedMethods.cs b/src/LibRyujinx/Android/JniExportedMethods.cs index c155e29d2..9cc8e848c 100644 --- a/src/LibRyujinx/Android/JniExportedMethods.cs +++ b/src/LibRyujinx/Android/JniExportedMethods.cs @@ -42,11 +42,36 @@ namespace LibRyujinx private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch); [DllImport("libryujinxjni")] - private extern static long storeString(string ch); - [DllImport("libryujinxjni")] - private extern static IntPtr getString(long id); + internal extern static long storeString(string ch); - private static string GetStoredString(long id) + [DllImport("libryujinxjni")] + internal extern static IntPtr getString(long id); + + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerTitle(long title); + + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerMessage(long message); + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerWatermark(long watermark); + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerInitialText(long text); + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerSubtitle(long text); + + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerType(int type); + + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerKeyboardMode(int mode); + + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerMinLength(int lenght); + + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerMaxLength(int lenght); + + internal static string GetStoredString(long id) { var pointer = getString(id); if (pointer != IntPtr.Zero) @@ -91,7 +116,7 @@ namespace LibRyujinx Logger.AddTarget( new AsyncLogTargetWrapper( - new AndroidLogTarget("Ryujinx"), + new AndroidLogTarget("RyujinxLog"), 1000, AsyncLogTargetOverflowAction.Block )); @@ -709,6 +734,30 @@ namespace LibRyujinx DeleteUser(userId); } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_uiHandlerSetup")] + public static void JniSetupUiHandler(JEnvRef jEnv, JObjectLocalRef jObj) + { + SetupUiHandler(); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_uiHandlerWait")] + public static void JniWaitUiHandler(JEnvRef jEnv, JObjectLocalRef jObj) + { + WaitUiHandler(); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_uiHandlerStopWait")] + public static void JniStopUiHandlerWait(JEnvRef jEnv, JObjectLocalRef jObj) + { + StopUiHandlerWait(); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_uiHandlerSetResponse")] + public static void JniSetUiHandlerResponse(JEnvRef jEnv, JObjectLocalRef jObj, JBoolean isOkPressed, JLong input) + { + SetUiHandlerResponse(isOkPressed, input); + } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userOpenUser")] public static void JniOpenUser(JEnvRef jEnv, JObjectLocalRef jObj, JLong userIdPtr) { diff --git a/src/LibRyujinx/LibRyujinx.cs b/src/LibRyujinx/LibRyujinx.cs index 1e27a9230..e8c83a338 100644 --- a/src/LibRyujinx/LibRyujinx.cs +++ b/src/LibRyujinx/LibRyujinx.cs @@ -37,6 +37,8 @@ using System.Collections.Generic; using LibHac.Bcat; using Ryujinx.Ui.App.Common; using System.Text; +using Ryujinx.HLE.Ui; +using LibRyujinx.Android; namespace LibRyujinx { @@ -89,7 +91,7 @@ namespace LibRyujinx Console.WriteLine(ex); return false; } - + OpenALLibraryNameContainer.OverridePath = "libopenal.so"; Logger.Notice.Print(LogClass.Application, "RyujinxAndroid is ready!"); @@ -142,7 +144,9 @@ namespace LibRyujinx var gameInfo = new GameInfo { - FileSize = gameStream.Length * 0.000000000931, TitleName = "Unknown", TitleId = "0000000000000000", + FileSize = gameStream.Length * 0.000000000931, + TitleName = "Unknown", + TitleId = "0000000000000000", Developer = "Unknown", Version = "0", Icon = null @@ -629,11 +633,11 @@ namespace LibRyujinx public static List GetDlcContentList(string path, ulong titleId) { - if(!File.Exists(path)) + if (!File.Exists(path)) return new List(); using FileStream containerFile = File.OpenRead(path); - + PartitionFileSystem partitionFileSystem = new(); partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); @@ -665,6 +669,38 @@ namespace LibRyujinx return paths; } + + public static void SetupUiHandler() + { + if (SwitchDevice is { } switchDevice) + { + switchDevice.HostUiHandler = new AndroidUiHandler(); + } + } + + public static void WaitUiHandler() + { + if (SwitchDevice?.HostUiHandler is AndroidUiHandler uiHandler) + { + uiHandler.Wait(); + } + } + + public static void StopUiHandlerWait() + { + if (SwitchDevice?.HostUiHandler is AndroidUiHandler uiHandler) + { + uiHandler.Set(); + } + } + + public static void SetUiHandlerResponse(bool isOkPressed, long input) + { + if (SwitchDevice?.HostUiHandler is AndroidUiHandler uiHandler) + { + uiHandler.SetResponse(isOkPressed, input); + } + } } public class SwitchDevice : IDisposable @@ -677,6 +713,7 @@ namespace LibRyujinx public UserChannelPersistence UserChannelPersistence { get; set; } public InputManager? InputManager { get; set; } public Switch? EmulationContext { get; set; } + public IHostUiHandler? HostUiHandler { get; set; } public void Dispose() { @@ -741,7 +778,7 @@ namespace LibRyujinx renderer, LibRyujinx.AudioDriver, //Audio MemoryConfiguration.MemoryConfiguration4GiB, - null, + HostUiHandler, systemLanguage, regionCode, enableVsync, diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h index 4a2859659..951d36385 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h +++ b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h @@ -38,6 +38,36 @@ void* _ryujinxNative = NULL; +class UiHandler { +public: + void setTitle(long storedTitle); + void setMessage(long storedMessage); + void setWatermark(long wm); + void setType(int t); + void setMode(int t); + void setMinLength(int t); + void setMaxLength(int t); + void setInitialText(long text); + void setSubtitle(long text); + + long getTitle(); + long getMessage(); + long getWatermark(); + long getInitialText(); + long getSubtitle(); + int type = 0; + int keyboardMode = 0; + int min_length = -1; + int max_length = -1; + +private: + long title = -1; + long message = -1; + long watermark = -1; + long initialText = -1; + long subtitle = -1; +}; + // Ryujinx imported functions bool (*initialize)(char*) = NULL; @@ -46,7 +76,7 @@ long _currentRenderingThreadId = 0; JavaVM* _vm = nullptr; jobject _mainActivity = nullptr; jclass _mainActivityClass = nullptr; -std::string _currentString = ""; string_helper str_helper = string_helper(); +UiHandler ui_handler = UiHandler(); #endif //RYUJINXNATIVE_RYUIJNX_H diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp index 19f01679d..da87b8051 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp +++ b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp @@ -327,6 +327,37 @@ const char* getString(long id){ return cstr; } +extern "C" +{ +void setUiHandlerTitle(long title) { + ui_handler.setTitle(title); +} +void setUiHandlerMessage(long message) { + ui_handler.setMessage(message); +} +void setUiHandlerWatermark(long wm) { + ui_handler.setWatermark(wm); +} +void setUiHandlerType(int type) { + ui_handler.setType(type); +} +void setUiHandlerKeyboardMode(int mode) { + ui_handler.setMode(mode); +} +void setUiHandlerMinLength(int length) { + ui_handler.setMinLength(length); +} +void setUiHandlerMaxLength(int length) { + ui_handler.setMaxLength(length); +} +void setUiHandlerInitialText(long text) { + ui_handler.setInitialText(text); +} +void setUiHandlerSubtitle(long text) { + ui_handler.setSubtitle(text); +} +} + extern "C" JNIEXPORT jlong JNICALL Java_org_ryujinx_android_NativeHelpers_storeStringJava(JNIEnv *env, jobject thiz, jstring string) { @@ -337,7 +368,7 @@ Java_org_ryujinx_android_NativeHelpers_storeStringJava(JNIEnv *env, jobject thiz extern "C" JNIEXPORT jstring JNICALL Java_org_ryujinx_android_NativeHelpers_getStringJava(JNIEnv *env, jobject thiz, jlong id) { - return createStringFromStdString(env, str_helper.get_stored(id)); + return createStringFromStdString(env, id > -1 ? str_helper.get_stored(id) : ""); } extern "C" @@ -346,3 +377,145 @@ Java_org_ryujinx_android_NativeHelpers_setIsInitialOrientationFlipped(JNIEnv *en jboolean is_flipped) { isInitialOrientationFlipped = is_flipped; } + +extern "C" +JNIEXPORT jint JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestType(JNIEnv *env, jobject thiz) { + return ui_handler.type; +} +extern "C" +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestTitle(JNIEnv *env, jobject thiz) { + return ui_handler.getTitle(); +} +extern "C" +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestMessage(JNIEnv *env, jobject thiz) { + return ui_handler.getMessage(); +} + + +void UiHandler::setTitle(long storedTitle) { + if(title != -1){ + str_helper.get_stored(title); + title = -1; + } + + title = storedTitle; +} + +void UiHandler::setMessage(long storedMessage) { + if(message != -1){ + str_helper.get_stored(message); + message = -1; + } + + message = storedMessage; +} + +void UiHandler::setType(int t) { + this->type = t; +} + +long UiHandler::getTitle() { + auto v = title; + title = -1; + return v; +} + +long UiHandler::getMessage() { + auto v = message; + message = -1; + return v; +} + +void UiHandler::setWatermark(long wm) { + if(watermark != -1){ + str_helper.get_stored(watermark); + watermark = -1; + } + + watermark = wm; +} + +void UiHandler::setMinLength(int t) { + this->min_length = t; +} + +void UiHandler::setMaxLength(int t) { + this->max_length = t; +} + +long UiHandler::getWatermark() { + auto v = watermark; + watermark = -1; + return v; +} + +void UiHandler::setInitialText(long text) { + if(initialText != -1){ + str_helper.get_stored(watermark); + initialText = -1; + } + + initialText = text; +} + +void UiHandler::setSubtitle(long text) { + if(subtitle != -1){ + str_helper.get_stored(subtitle); + subtitle = -1; + } + + subtitle = text; +} + +long UiHandler::getInitialText() { + auto v = initialText; + initialText = -1; + return v; +} + +long UiHandler::getSubtitle() { + auto v = subtitle; + subtitle = -1; + return v; +} + +void UiHandler::setMode(int t) { + keyboardMode = t; +} + +extern "C" +JNIEXPORT jint JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerMinLength(JNIEnv *env, jobject thiz) { + return ui_handler.min_length; +} +extern "C" +JNIEXPORT jint JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerMaxLength(JNIEnv *env, jobject thiz) { + return ui_handler.max_length; +} + +extern "C" +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestWatermark(JNIEnv *env, jobject thiz) { + return ui_handler.getWatermark(); +} + +extern "C" +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestInitialText(JNIEnv *env, jobject thiz) { + return ui_handler.getInitialText(); +} +extern "C" +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestSubtitle(JNIEnv *env, jobject thiz) { + return ui_handler.getSubtitle(); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerKeyboardMode(JNIEnv *env, jobject thiz) { + return ui_handler.keyboardMode; +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt index eaa9042db..9eca1df52 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt @@ -75,6 +75,8 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su _isInit = false _isStarted = false + mainViewModel.activity.uiHandler.stop() + _updateThread?.join() _renderingThreadWatcher?.join() } @@ -161,6 +163,10 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su mainViewModel.performanceManager?.closeCurrentRenderingSession() } } + + thread { + mainViewModel.activity.uiHandler.listen() + } _nativeRyujinx.graphicsRendererRunLoop() game?.close() diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt index 70d1eb2b6..b2fe80556 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt @@ -18,6 +18,7 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import com.anggrayudi.storage.SimpleStorageHelper +import com.halilibo.richtext.ui.RichTextThemeIntegration import org.ryujinx.android.ui.theme.RyujinxAndroidTheme import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.QuickSettings @@ -32,6 +33,7 @@ class MainActivity : BaseActivity() { private var _isInit: Boolean = false var isGameRunning = false var storageHelper: SimpleStorageHelper? = null + lateinit var uiHandler: UiHandler companion object { var mainViewModel: MainViewModel? = null var AppPath : String = "" @@ -77,6 +79,8 @@ class MainActivity : BaseActivity() { RyujinxNative.instance.loggingSetEnabled(LogLevel.Guest.ordinal, quickSettings.enableGuestLogs) RyujinxNative.instance.loggingSetEnabled(LogLevel.Trace.ordinal, quickSettings.enableTraceLogs) val success = RyujinxNative.instance.initialize(NativeHelpers.instance.storeStringJava(appPath)) + + uiHandler = UiHandler() _isInit = success } override fun onCreate(savedInstanceState: Bundle?) { @@ -107,12 +111,14 @@ class MainActivity : BaseActivity() { mainViewModel?.apply { setContent { RyujinxAndroidTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - MainView.Main(mainViewModel = this) + RichTextThemeIntegration(contentColor = { MaterialTheme.colorScheme.onSurface }) { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + MainView.Main(mainViewModel = this) + } } } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt index cc3cbde95..9518435f3 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt @@ -32,4 +32,13 @@ class NativeHelpers { external fun storeStringJava(string: String) : Long external fun getStringJava(id: Long) : String external fun setIsInitialOrientationFlipped(isFlipped: Boolean) + external fun getUiHandlerRequestType() : Int + external fun getUiHandlerRequestTitle() : Long + external fun getUiHandlerRequestMessage() : Long + external fun getUiHandlerMinLength() : Int + external fun getUiHandlerMaxLength() : Int + external fun getUiHandlerKeyboardMode() : Int + external fun getUiHandlerRequestWatermark() : Long + external fun getUiHandlerRequestInitialText() : Long + external fun getUiHandlerRequestSubtitle() : Long } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt index 25ba5d562..fc3447d49 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt @@ -74,4 +74,8 @@ class RyujinxNative { external fun deviceVerifyFirmware(fileDescriptor: Int, isXci: Boolean): Long external fun deviceInstallFirmware(fileDescriptor: Int, isXci: Boolean) external fun deviceGetInstalledFirmwareVersion() : Long + external fun uiHandlerSetup() + external fun uiHandlerWait() + external fun uiHandlerStopWait() + external fun uiHandlerSetResponse(isOkPressed: Boolean, input: Long) } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/UiHandler.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/UiHandler.kt new file mode 100644 index 000000000..f1b8e2cef --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/UiHandler.kt @@ -0,0 +1,215 @@ +package org.ryujinx.android + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.ui.RichText + +internal enum class KeyboardMode { + Default, Numeric, ASCII, FullLatin, Alphabet, SimplifiedChinese, TraditionalChinese, Korean, LanguageSet2, LanguageSet2Latin +} + +class UiHandler { + private var initialText: String = "" + private var subtitle: String = "" + private var maxLength: Int = 0 + private var minLength: Int = 0 + private var watermark: String = "" + private var type: Int = -1 + private var mode: KeyboardMode = KeyboardMode.Default + val showMessage = mutableStateOf(false) + val inputText = mutableStateOf("") + var title: String = "" + var message: String = "" + var shouldListen = true + + init { + RyujinxNative.instance.uiHandlerSetup() + } + + fun listen() { + showMessage.value = false + while (shouldListen) { + RyujinxNative.instance.uiHandlerWait() + + title = + NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestTitle()) + message = + NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestMessage()) + watermark = + NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestWatermark()) + type = NativeHelpers.instance.getUiHandlerRequestType() + minLength = NativeHelpers.instance.getUiHandlerMinLength() + maxLength = NativeHelpers.instance.getUiHandlerMaxLength() + mode = KeyboardMode.values()[NativeHelpers.instance.getUiHandlerKeyboardMode()] + subtitle = + NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestSubtitle()) + initialText = + NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestInitialText()) + inputText.value = initialText + showMessage.value = type > 0 + } + } + + fun stop() { + shouldListen = false + RyujinxNative.instance.uiHandlerStopWait() + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun Compose() { + val showMessageListener = remember { + showMessage + } + + val inputListener = remember { + inputText + } + val validation = remember { + mutableStateOf("") + } + + fun validate() : Boolean{ + if(inputText.value.isEmpty()){ + validation.value = "Must be between ${minLength} and ${maxLength} characters" + } + else{ + return inputText.value.length < minLength || inputText.value.length > maxLength + } + + return false; + } + + fun getInputType(): KeyboardType { + return when(mode){ + KeyboardMode.Default -> KeyboardType.Text + KeyboardMode.Numeric -> KeyboardType.Decimal + KeyboardMode.ASCII -> KeyboardType.Ascii + else -> { KeyboardType.Text} + } + } + + fun submit() { + var input: Long = -1 + if (type == 2) { + if (inputListener.value.length < minLength || inputListener.value.length > maxLength) + return + input = + NativeHelpers.instance.storeStringJava(inputListener.value) + } + showMessageListener.value = false + RyujinxNative.instance.uiHandlerSetResponse(true, input) + } + + if (showMessageListener.value) { + AlertDialog( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + onDismissRequest = { }, + properties = DialogProperties(dismissOnBackPress = false, false) + ) { + Column { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column( + modifier = Modifier + .padding(16.dp) + ) { + Text(text = title) + Column( + modifier = Modifier + .height(128.dp) + .verticalScroll(rememberScrollState()) + .padding(8.dp), + verticalArrangement = Arrangement.Center + ) { + RichText { + Markdown(content = message) + } + if (type == 2) { + validate() + if (watermark.isNotEmpty()) + TextField( + value = inputListener.value, + onValueChange = { inputListener.value = it }, + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + label = { + Text(text = watermark) + }, + keyboardOptions = KeyboardOptions(keyboardType = getInputType()), + isError = validate() + ) + else + TextField( + value = inputListener.value, + onValueChange = { inputListener.value = it }, + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + keyboardOptions = KeyboardOptions( + keyboardType = getInputType(), + imeAction = ImeAction.Done + ), + isError = validate(), + singleLine = true, + keyboardActions = KeyboardActions(onDone = { submit() }) + ) + if (subtitle.isNotEmpty()) + Text(text = subtitle) + Text(text = validation.value) + } + } + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + ) { + Button(onClick = { + submit() + }) { + Text(text = "OK") + } + } + } + } + } + } + } + } +} + diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt index e3b153530..6ca0ab89e 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt @@ -285,6 +285,8 @@ class GameViews { } } } + + mainViewModel.activity.uiHandler.Compose() } }