committed by
bunnei
6 changed files with 255 additions and 279 deletions
-
14src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
-
264src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/SoftwareKeyboard.java
-
117src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt
-
100src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/ui/KeyboardDialogFragment.kt
-
16src/android/app/src/main/jni/applets/software_keyboard.cpp
-
23src/android/app/src/main/res/layout/dialog_edit_text.xml
@ -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); |
|||
} |
|||
} |
|||
@ -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) |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue