yuzu-emu
/
yuzu-mainline
Archived
1
0
Fork 0

android: Convert keyboard applet to kotlin and refactor

This commit is contained in:
Charles Lombardo 2023-03-31 21:28:49 -04:00 committed by bunnei
parent d5ebfc8e21
commit d30103b69f
6 changed files with 255 additions and 279 deletions

View File

@ -83,22 +83,22 @@ open class EmulationActivity : AppCompatActivity() {
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (event.action == android.view.KeyEvent.ACTION_DOWN) { if (event.action == KeyEvent.ACTION_DOWN) {
if (keyCode == android.view.KeyEvent.KEYCODE_ENTER) { if (keyCode == KeyEvent.KEYCODE_ENTER) {
// Special case, we do not support multiline input, dismiss the keyboard. // Special case, we do not support multiline input, dismiss the keyboard.
val overlayView: View = val overlayView: View =
this.findViewById<View>(R.id.surface_input_overlay) this.findViewById(R.id.surface_input_overlay)
val im = val im =
overlayView.context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager overlayView.context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
im.hideSoftInputFromWindow(overlayView.windowToken, 0); im.hideSoftInputFromWindow(overlayView.windowToken, 0)
} else { } else {
val textChar = event.getUnicodeChar(); val textChar = event.unicodeChar
if (textChar == 0) { if (textChar == 0) {
// No text, button input. // No text, button input.
NativeLibrary.SubmitInlineKeyboardInput(keyCode); NativeLibrary.SubmitInlineKeyboardInput(keyCode)
} else { } else {
// Text submitted. // Text submitted.
NativeLibrary.SubmitInlineKeyboardText(textChar.toChar().toString()); NativeLibrary.SubmitInlineKeyboardText(textChar.toChar().toString())
} }
} }
} }

View File

@ -1,264 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.applets;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import android.text.InputFilter;
import android.text.InputType;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.yuzu.yuzu_emu.YuzuApplication;
import org.yuzu.yuzu_emu.NativeLibrary;
import org.yuzu.yuzu_emu.R;
import org.yuzu.yuzu_emu.activities.EmulationActivity;
import java.util.Objects;
public final class SoftwareKeyboard {
/// Corresponds to Service::AM::Applets::SwkbdType
private interface SwkbdType {
int Normal = 0;
int NumberPad = 1;
int Qwerty = 2;
int Unknown3 = 3;
int Latin = 4;
int SimplifiedChinese = 5;
int TraditionalChinese = 6;
int Korean = 7;
};
/// Corresponds to Service::AM::Applets::SwkbdPasswordMode
private interface SwkbdPasswordMode {
int Disabled = 0;
int Enabled = 1;
};
/// Corresponds to Service::AM::Applets::SwkbdResult
private interface SwkbdResult {
int Ok = 0;
int Cancel = 1;
};
public static class KeyboardConfig implements java.io.Serializable {
public String ok_text;
public String header_text;
public String sub_text;
public String guide_text;
public String initial_text;
public short left_optional_symbol_key;
public short right_optional_symbol_key;
public int max_text_length;
public int min_text_length;
public int initial_cursor_position;
public int type;
public int password_mode;
public int text_draw_type;
public int key_disable_flags;
public boolean use_blur_background;
public boolean enable_backspace_button;
public boolean enable_return_button;
public boolean disable_cancel_button;
}
/// Corresponds to Frontend::KeyboardData
public static class KeyboardData {
public int result;
public String text;
private KeyboardData(int result, String text) {
this.result = result;
this.text = text;
}
}
public static class KeyboardDialogFragment extends DialogFragment {
static KeyboardDialogFragment newInstance(KeyboardConfig config) {
KeyboardDialogFragment frag = new KeyboardDialogFragment();
Bundle args = new Bundle();
args.putSerializable("config", config);
frag.setArguments(args);
return frag;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity emulationActivity = getActivity();
assert emulationActivity != null;
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.leftMargin = params.rightMargin =
YuzuApplication.getAppContext().getResources().getDimensionPixelSize(
R.dimen.dialog_margin);
KeyboardConfig config = Objects.requireNonNull(
(KeyboardConfig) requireArguments().getSerializable("config"));
// Set up the input
EditText editText = new EditText(YuzuApplication.getAppContext());
editText.setHint(config.initial_text);
editText.setSingleLine(!config.enable_return_button);
editText.setLayoutParams(params);
editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(config.max_text_length)});
// Handle input type
int input_type = 0;
switch (config.type)
{
case SwkbdType.Normal:
case SwkbdType.Qwerty:
case SwkbdType.Unknown3:
case SwkbdType.Latin:
case SwkbdType.SimplifiedChinese:
case SwkbdType.TraditionalChinese:
case SwkbdType.Korean:
default:
input_type = InputType.TYPE_CLASS_TEXT;
if (config.password_mode == SwkbdPasswordMode.Enabled)
{
input_type |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
}
break;
case SwkbdType.NumberPad:
input_type = InputType.TYPE_CLASS_NUMBER;
if (config.password_mode == SwkbdPasswordMode.Enabled)
{
input_type |= InputType.TYPE_NUMBER_VARIATION_PASSWORD;
}
break;
}
// Apply input type
editText.setInputType(input_type);
FrameLayout container = new FrameLayout(emulationActivity);
container.addView(editText);
String headerText = config.header_text.isEmpty() ? emulationActivity.getString(R.string.software_keyboard) : config.header_text;
String okText = config.header_text.isEmpty() ? emulationActivity.getString(android.R.string.ok) : config.ok_text;
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
.setTitle(headerText)
.setView(container);
setCancelable(false);
builder.setPositiveButton(okText, null);
builder.setNegativeButton(emulationActivity.getString(android.R.string.cancel), null);
final AlertDialog dialog = builder.create();
dialog.create();
if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> {
data.result = SwkbdResult.Ok;
data.text = editText.getText().toString();
dialog.dismiss();
synchronized (finishLock) {
finishLock.notifyAll();
}
});
}
if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) {
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> {
data.result = SwkbdResult.Ok;
dialog.dismiss();
synchronized (finishLock) {
finishLock.notifyAll();
}
});
}
if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) {
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> {
data.result = SwkbdResult.Cancel;
dialog.dismiss();
synchronized (finishLock) {
finishLock.notifyAll();
}
});
}
return dialog;
}
}
private static KeyboardData data;
private static final Object finishLock = new Object();
private static void ExecuteNormalImpl(KeyboardConfig config) {
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
data = new KeyboardData(SwkbdResult.Cancel, "");
KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config);
fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard");
}
private static void ExecuteInlineImpl(KeyboardConfig config) {
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
var overlayView = emulationActivity.findViewById(R.id.surface_input_overlay);
InputMethodManager im = (InputMethodManager)overlayView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
im.showSoftInput(overlayView, InputMethodManager.SHOW_FORCED);
// There isn't a good way to know that the IMM is dismissed, so poll every 500ms to submit inline keyboard result.
final Handler handler = new Handler();
final int delayMs = 500;
handler.postDelayed(new Runnable() {
public void run() {
var insets = ViewCompat.getRootWindowInsets(overlayView);
var isKeyboardVisible = insets.isVisible(WindowInsets.Type.ime());
if (isKeyboardVisible) {
handler.postDelayed(this, delayMs);
return;
}
// No longer visible, submit the result.
NativeLibrary.SubmitInlineKeyboardInput(android.view.KeyEvent.KEYCODE_ENTER);
}
}, delayMs);
}
public static KeyboardData ExecuteNormal(KeyboardConfig config) {
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteNormalImpl(config));
synchronized (finishLock) {
try {
finishLock.wait();
} catch (Exception ignored) {
}
}
return data;
}
public static void ExecuteInline(KeyboardConfig config) {
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteInlineImpl(config));
}
public static void ShowError(String error) {
NativeLibrary.displayAlertMsg(
YuzuApplication.getAppContext().getResources().getString(R.string.software_keyboard),
error, false);
}
}

View File

@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.applets.keyboard
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.KeyEvent
import android.view.View
import android.view.WindowInsets
import android.view.inputmethod.InputMethodManager
import androidx.core.view.ViewCompat
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.applets.keyboard.ui.KeyboardDialogFragment
import java.io.Serializable
object SoftwareKeyboard {
lateinit var data: KeyboardData
val dataLock = Object()
private fun executeNormalImpl(config: KeyboardConfig) {
val emulationActivity = NativeLibrary.sEmulationActivity.get()
data = KeyboardData(SwkbdResult.Cancel.ordinal, "")
val fragment = KeyboardDialogFragment.newInstance(config)
fragment.show(emulationActivity!!.supportFragmentManager, KeyboardDialogFragment.TAG)
}
private fun executeInlineImpl(config: KeyboardConfig) {
val emulationActivity = NativeLibrary.sEmulationActivity.get()
val overlayView = emulationActivity!!.findViewById<View>(R.id.surface_input_overlay)
val im =
overlayView.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
im.showSoftInput(overlayView, InputMethodManager.SHOW_FORCED)
// There isn't a good way to know that the IMM is dismissed, so poll every 500ms to submit inline keyboard result.
val handler = Handler(Looper.myLooper()!!)
val delayMs = 500
handler.postDelayed(object : Runnable {
override fun run() {
val insets = ViewCompat.getRootWindowInsets(overlayView)
val isKeyboardVisible = insets!!.isVisible(WindowInsets.Type.ime())
if (isKeyboardVisible) {
handler.postDelayed(this, delayMs.toLong())
return
}
// No longer visible, submit the result.
NativeLibrary.SubmitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER)
}
}, delayMs.toLong())
}
@JvmStatic
fun executeNormal(config: KeyboardConfig): KeyboardData {
NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { executeNormalImpl(config) }
synchronized(dataLock) {
dataLock.wait()
}
return data
}
@JvmStatic
fun executeInline(config: KeyboardConfig) {
NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { executeInlineImpl(config) }
}
// Corresponds to Service::AM::Applets::SwkbdType
enum class SwkbdType {
Normal,
NumberPad,
Qwerty,
Unknown3,
Latin,
SimplifiedChinese,
TraditionalChinese,
Korean
}
// Corresponds to Service::AM::Applets::SwkbdPasswordMode
enum class SwkbdPasswordMode {
Disabled,
Enabled
}
// Corresponds to Service::AM::Applets::SwkbdResult
enum class SwkbdResult {
Ok,
Cancel
}
data class KeyboardConfig(
var ok_text: String? = null,
var header_text: String? = null,
var sub_text: String? = null,
var guide_text: String? = null,
var initial_text: String? = null,
var left_optional_symbol_key: Short = 0,
var right_optional_symbol_key: Short = 0,
var max_text_length: Int = 0,
var min_text_length: Int = 0,
var initial_cursor_position: Int = 0,
var type: Int = 0,
var password_mode: Int = 0,
var text_draw_type: Int = 0,
var key_disable_flags: Int = 0,
var use_blur_background: Boolean = false,
var enable_backspace_button: Boolean = false,
var enable_return_button: Boolean = false,
var disable_cancel_button: Boolean = false
) : Serializable
// Corresponds to Frontend::KeyboardData
data class KeyboardData(var result: Int, var text: String)
}

View File

@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.applets.keyboard.ui
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.text.InputFilter
import android.text.InputType
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.applets.keyboard.SoftwareKeyboard
import org.yuzu.yuzu_emu.applets.keyboard.SoftwareKeyboard.KeyboardConfig
import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
class KeyboardDialogFragment : DialogFragment() {
private lateinit var binding: DialogEditTextBinding
private lateinit var config: KeyboardConfig
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogEditTextBinding.inflate(layoutInflater)
config = requireArguments().serializable(CONFIG)!!
// Set up the input
binding.editText.hint = config.initial_text
binding.editText.isSingleLine = !config.enable_return_button
binding.editText.filters =
arrayOf<InputFilter>(InputFilter.LengthFilter(config.max_text_length))
// Handle input type
var inputType: Int
when (config.type) {
SoftwareKeyboard.SwkbdType.Normal.ordinal,
SoftwareKeyboard.SwkbdType.Qwerty.ordinal,
SoftwareKeyboard.SwkbdType.Unknown3.ordinal,
SoftwareKeyboard.SwkbdType.Latin.ordinal,
SoftwareKeyboard.SwkbdType.SimplifiedChinese.ordinal,
SoftwareKeyboard.SwkbdType.TraditionalChinese.ordinal,
SoftwareKeyboard.SwkbdType.Korean.ordinal -> {
inputType = InputType.TYPE_CLASS_TEXT
if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) {
inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
}
SoftwareKeyboard.SwkbdType.NumberPad.ordinal -> {
inputType = InputType.TYPE_CLASS_NUMBER
if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) {
inputType = inputType or InputType.TYPE_NUMBER_VARIATION_PASSWORD
}
}
else -> {
inputType = InputType.TYPE_CLASS_TEXT
if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) {
inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
}
}
binding.editText.inputType = inputType
val headerText =
config.header_text!!.ifEmpty { resources.getString(R.string.software_keyboard) }
val okText =
if (config.header_text!!.isEmpty()) resources.getString(android.R.string.ok) else config.ok_text!!
return MaterialAlertDialogBuilder(requireContext())
.setTitle(headerText)
.setView(binding.root)
.setPositiveButton(okText) { _, _ ->
SoftwareKeyboard.data.result = SoftwareKeyboard.SwkbdResult.Ok.ordinal
SoftwareKeyboard.data.text = binding.editText.text.toString()
}
.setNegativeButton(resources.getString(android.R.string.cancel)) { _, _ ->
SoftwareKeyboard.data.result = SoftwareKeyboard.SwkbdResult.Cancel.ordinal
}
.create()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
synchronized(SoftwareKeyboard.dataLock) {
SoftwareKeyboard.dataLock.notifyAll()
}
}
companion object {
const val TAG = "KeyboardDialogFragment"
const val CONFIG = "keyboard_config"
fun newInstance(config: KeyboardConfig?): KeyboardDialogFragment {
val frag = KeyboardDialogFragment()
val args = Bundle()
args.putSerializable(CONFIG, config)
frag.arguments = args
return frag
}
}
}

View File

@ -253,19 +253,19 @@ void AndroidKeyboard::SubmitNormalText(const ResultData& data) const {
void InitJNI(JNIEnv* env) { void InitJNI(JNIEnv* env) {
s_software_keyboard_class = reinterpret_cast<jclass>( s_software_keyboard_class = reinterpret_cast<jclass>(
env->NewGlobalRef(env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard"))); env->NewGlobalRef(env->FindClass("org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard")));
s_keyboard_config_class = reinterpret_cast<jclass>(env->NewGlobalRef( s_keyboard_config_class = reinterpret_cast<jclass>(env->NewGlobalRef(
env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig"))); env->FindClass("org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardConfig")));
s_keyboard_data_class = reinterpret_cast<jclass>(env->NewGlobalRef( s_keyboard_data_class = reinterpret_cast<jclass>(env->NewGlobalRef(
env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardData"))); env->FindClass("org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardData")));
s_swkbd_execute_normal = env->GetStaticMethodID( s_swkbd_execute_normal = env->GetStaticMethodID(
s_software_keyboard_class, "ExecuteNormal", s_software_keyboard_class, "executeNormal",
"(Lorg/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig;)Lorg/yuzu/yuzu_emu/" "(Lorg/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardConfig;)Lorg/yuzu/yuzu_emu/"
"applets/SoftwareKeyboard$KeyboardData;"); "applets/keyboard/SoftwareKeyboard$KeyboardData;");
s_swkbd_execute_inline = s_swkbd_execute_inline =
env->GetStaticMethodID(s_software_keyboard_class, "ExecuteInline", env->GetStaticMethodID(s_software_keyboard_class, "executeInline",
"(Lorg/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig;)V"); "(Lorg/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardConfig;)V");
} }
void CleanupJNI(JNIEnv* env) { void CleanupJNI(JNIEnv* env) {

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/edit_text_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="24dp"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>