[VrKeyboardActivity] Add a hacky in-app keyboard that won't crash

This commit is contained in:
amwatson 2024-01-18 15:48:25 -06:00
parent 5fec9c3579
commit c90d0d27de
12 changed files with 865 additions and 162 deletions

View file

@ -126,6 +126,22 @@
</activity> </activity>
<activity
android:name="org.citra.citra_emu.vr.VrKeyboardActivity"
android:exported="true"
android:process=":vr_process"
android:windowSoftInputMode="stateVisible|adjustResize"
android:resizeableActivity="false"
android:screenOrientation="landscape">
<!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="com.oculus.intent.category.2D" />
</intent-filter>
</activity>
<service android:name="org.citra.citra_emu.utils.ForegroundService" /> <service android:name="org.citra.citra_emu.utils.ForegroundService" />
<activity <activity

View file

@ -7,31 +7,34 @@ package org.citra.citra_emu.applets;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.text.InputFilter; import android.text.InputFilter;
import android.text.Spanned; import android.text.Spanned;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.EditText; import android.widget.EditText;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.Objects;
import org.citra.citra_emu.CitraApplication; import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.NativeLibrary; import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R; import org.citra.citra_emu.R;
import org.citra.citra_emu.activities.EmulationActivity; import org.citra.citra_emu.activities.EmulationActivity;
import org.citra.citra_emu.utils.Log; import org.citra.citra_emu.utils.Log;
import org.citra.citra_emu.vr.VrActivity;
import org.citra.citra_emu.vr.VrKeyboardActivity;
import java.util.Objects;
// Warning (amwatson): I had to tear through this pretty quickly because I didn't realize
// there was a system keyboard. This is a pretty hack solution that will not
// merge well.
public final class SoftwareKeyboard { public final class SoftwareKeyboard {
/// Corresponds to Frontend::ButtonConfig /// Corresponds to Frontend::ButtonConfig
private interface ButtonConfig { public interface ButtonConfig {
int Single = 0; /// Ok button int Single = 0; /// Ok button
int Dual = 1; /// Cancel | Ok buttons int Dual = 1; /// Cancel | Ok buttons
int Triple = 2; /// Cancel | I Forgot | Ok buttons int Triple = 2; /// Cancel | I Forgot | Ok buttons
@ -62,8 +65,7 @@ public final class SoftwareKeyboard {
public int max_text_length; public int max_text_length;
public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input
public String hint_text; /// Displayed in the field as a hint before public String hint_text; /// Displayed in the field as a hint before
@Nullable @Nullable public String[] button_text; /// Contains the button text that the caller provides
public String[] button_text; /// Contains the button text that the caller provides
} }
/// Corresponds to Frontend::KeyboardData /// Corresponds to Frontend::KeyboardData
@ -77,13 +79,13 @@ public final class SoftwareKeyboard {
} }
} }
private static class Filter implements InputFilter { public static class Filter implements InputFilter {
@Override @Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
int dstart, int dend) { int dstart, int dend) {
String text = new StringBuilder(dest) String text = new StringBuilder(dest)
.replace(dstart, dend, source.subSequence(start, end).toString()) .replace(dstart, dend, source.subSequence(start, end).toString())
.toString(); .toString();
if (ValidateFilters(text) == ValidationError.None) { if (ValidateFilters(text) == ValidationError.None) {
return null; // Accept replacement return null; // Accept replacement
} }
@ -107,52 +109,52 @@ public final class SoftwareKeyboard {
assert emulationActivity != null; assert emulationActivity != null;
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.leftMargin = params.rightMargin = params.leftMargin = params.rightMargin =
CitraApplication.getAppContext().getResources().getDimensionPixelSize( CitraApplication.getAppContext().getResources().getDimensionPixelSize(
R.dimen.dialog_margin); R.dimen.dialog_margin);
KeyboardConfig config = Objects.requireNonNull( KeyboardConfig config = Objects.requireNonNull(
(KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config")); (KeyboardConfig)Objects.requireNonNull(getArguments()).getSerializable("config"));
// Set up the input // Set up the input
EditText editText = new EditText(CitraApplication.getAppContext()); EditText editText = new EditText(CitraApplication.getAppContext());
editText.setHint(config.hint_text); editText.setHint(config.hint_text);
editText.setSingleLine(!config.multiline_mode); editText.setSingleLine(!config.multiline_mode);
editText.setLayoutParams(params); editText.setLayoutParams(params);
editText.setFilters(new InputFilter[]{ editText.setFilters(new InputFilter[] {
new Filter(), new InputFilter.LengthFilter(config.max_text_length)}); new Filter(), new InputFilter.LengthFilter(config.max_text_length)});
FrameLayout container = new FrameLayout(emulationActivity); FrameLayout container = new FrameLayout(emulationActivity);
container.addView(editText); container.addView(editText);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity) MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
.setTitle(R.string.software_keyboard) .setTitle(R.string.software_keyboard)
.setView(container); .setView(container);
setCancelable(false); setCancelable(false);
switch (config.button_config) { switch (config.button_config) {
case ButtonConfig.Triple: { case ButtonConfig.Triple: {
final String text = config.button_text[1].isEmpty() final String text = config.button_text[1].isEmpty()
? emulationActivity.getString(R.string.i_forgot) ? emulationActivity.getString(R.string.i_forgot)
: config.button_text[1]; : config.button_text[1];
builder.setNeutralButton(text, null); builder.setNeutralButton(text, null);
} }
// fallthrough // fallthrough
case ButtonConfig.Dual: { case ButtonConfig.Dual: {
final String text = config.button_text[0].isEmpty() final String text = config.button_text[0].isEmpty()
? emulationActivity.getString(android.R.string.cancel) ? emulationActivity.getString(android.R.string.cancel)
: config.button_text[0]; : config.button_text[0];
builder.setNegativeButton(text, null); builder.setNegativeButton(text, null);
} }
// fallthrough // fallthrough
case ButtonConfig.Single: { case ButtonConfig.Single: {
final String text = config.button_text[2].isEmpty() final String text = config.button_text[2].isEmpty()
? emulationActivity.getString(android.R.string.ok) ? emulationActivity.getString(android.R.string.ok)
: config.button_text[2]; : config.button_text[2];
builder.setPositiveButton(text, null); builder.setPositiveButton(text, null);
break; break;
} }
} }
final AlertDialog dialog = builder.create(); final AlertDialog dialog = builder.create();
@ -209,31 +211,65 @@ public final class SoftwareKeyboard {
fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard"); fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard");
} }
private static void HandleValidationError(KeyboardConfig config, ValidationError error) { public static void HandleValidationError(KeyboardConfig config, ValidationError error) {
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
String message = ""; String message = "";
switch (error) { switch (error) {
case FixedLengthRequired: case FixedLengthRequired:
message = message =
emulationActivity.getString(R.string.fixed_length_required, config.max_text_length); emulationActivity.getString(R.string.fixed_length_required, config.max_text_length);
break; break;
case MaxLengthExceeded: case MaxLengthExceeded:
message = message =
emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length); emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length);
break; break;
case BlankInputNotAllowed: case BlankInputNotAllowed:
message = emulationActivity.getString(R.string.blank_input_not_allowed); message = emulationActivity.getString(R.string.blank_input_not_allowed);
break; break;
case EmptyInputNotAllowed: case EmptyInputNotAllowed:
message = emulationActivity.getString(R.string.empty_input_not_allowed); message = emulationActivity.getString(R.string.empty_input_not_allowed);
break; break;
} }
new MaterialAlertDialogBuilder(emulationActivity) // TODO show error dialog
.setTitle(R.string.software_keyboard) Log.warning("Keyboard error: " + message);
.setMessage(message) /* new MaterialAlertDialogBuilder(emulationActivity)
.setPositiveButton(android.R.string.ok, null) .setTitle(R.string.software_keyboard)
.show(); .setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.show();*/
}
public static void onFinishVrKeyboardPositive(final String text, final KeyboardConfig config) {
data = new KeyboardData(0, "");
data.button = config.button_config;
data.text = text;
final ValidationError error = ValidateInput(data.text);
if (error != ValidationError.None) {
HandleValidationError(config, error);
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
onFinishVrKeyboardNegative();
return;
}
synchronized (finishLock) {
finishLock.notifyAll();
}
}
public static void onFinishVrKeyboardNeutral() {
data = new KeyboardData(0, "");
data.button = 1;
synchronized (finishLock) {
finishLock.notifyAll();
}
}
public static void onFinishVrKeyboardNegative() {
data = new KeyboardData(0, "");
data.button = 0;
synchronized (finishLock) {
finishLock.notifyAll();
}
} }
public static KeyboardData Execute(KeyboardConfig config) { public static KeyboardData Execute(KeyboardConfig config) {
@ -242,7 +278,12 @@ public final class SoftwareKeyboard {
return new KeyboardData(0, ""); return new KeyboardData(0, "");
} }
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config)); final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
if (emulationActivity instanceof VrActivity) {
((VrActivity)emulationActivity).mVrKeyboardLauncher.launch(config);
} else {
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
}
synchronized (finishLock) { synchronized (finishLock) {
try { try {
@ -256,11 +297,11 @@ public final class SoftwareKeyboard {
public static void ShowError(String error) { public static void ShowError(String error) {
NativeLibrary.displayAlertMsg( NativeLibrary.displayAlertMsg(
CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard), CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard),
error, false); error, false);
} }
private static native ValidationError ValidateFilters(String text); public static native ValidationError ValidateFilters(String text);
private static native ValidationError ValidateInput(String text); public static native ValidationError ValidateInput(String text);
} }

View file

@ -1,19 +1,12 @@
package org.citra.citra_emu.vr; package org.citra.citra_emu.vr;
public class ErrorMessageLayer public class ErrorMessageLayer {
{
public static ErrorMessageLayer instance = null; public static ErrorMessageLayer instance = null;
public static void showErrorWindow(final String titleStr, public static void showErrorWindow(final String titleStr, final String mainMessageStr) {}
final String mainMessageStr)
{
}
public void _showErrorWindow(final String titleStr, public void _showErrorWindow(final String titleStr, final String mainMessageStr) {}
final String mainMessageStr)
{
}
public void hideErrorWindow() {} public void hideErrorWindow() {}

View file

@ -14,13 +14,11 @@ import org.citra.citra_emu.fragments.EmulationFragment;
* Note: this is set up to require the min number of changes possible to * Note: this is set up to require the min number of changes possible to
*existing Citra code, in case an upstream merge is desired. *existing Citra code, in case an upstream merge is desired.
**/ **/
public class GameSurfaceLayer public class GameSurfaceLayer {
{ public static void setSurface(VrActivity activity, Surface surface) {
public static void setSurface(VrActivity activity, Surface surface)
{
assert activity != null; assert activity != null;
((EmulationFragment)activity.getSupportFragmentManager() ((EmulationFragment)activity.getSupportFragmentManager().findFragmentById(
.findFragmentById(R.id.frame_emulation_fragment)) R.id.frame_emulation_fragment))
.surfaceCreated(surface); .surfaceCreated(surface);
} }
} }

View file

@ -10,52 +10,53 @@ import android.os.Bundle;
import android.view.Display; import android.view.Display;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.KeyEvent; import android.view.KeyEvent;
import androidx.activity.result.ActivityResultLauncher;
import org.citra.citra_emu.NativeLibrary; import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.activities.EmulationActivity; import org.citra.citra_emu.activities.EmulationActivity;
import org.citra.citra_emu.applets.SoftwareKeyboard;
import org.citra.citra_emu.features.settings.ui.SettingsActivity; import org.citra.citra_emu.features.settings.ui.SettingsActivity;
import org.citra.citra_emu.features.settings.utils.SettingsFile; import org.citra.citra_emu.features.settings.utils.SettingsFile;
import org.citra.citra_emu.utils.Log; import org.citra.citra_emu.utils.Log;
public class VrActivity extends EmulationActivity {
public class VrActivity extends EmulationActivity
{
private long mHandle = 0; private long mHandle = 0;
public static boolean hasRun = false; public static boolean hasRun = false;
public static VrActivity currentActivity = null; public static VrActivity currentActivity = null;
ClickRunnable clickRunnable = new ClickRunnable(); ClickRunnable clickRunnable = new ClickRunnable();
static { System.loadLibrary("openxr_forwardloader.oculus"); } static {
public static void launch(Context context, final String gamePath, System.loadLibrary("openxr_forwardloader.oculus");
final String gameTitle) }
{
public final ActivityResultLauncher<SoftwareKeyboard.KeyboardConfig> mVrKeyboardLauncher =
registerForActivityResult(new VrKeyboardActivity.Contract(),
result -> VrKeyboardActivity.onFinishResult(result));
public static void launch(Context context, final String gamePath, final String gameTitle) {
Intent intent = new Intent(context, VrActivity.class); Intent intent = new Intent(context, VrActivity.class);
final int mainDisplayId = getMainDisplay(context); final int mainDisplayId = getMainDisplay(context);
if (mainDisplayId < 0) if (mainDisplayId < 0) {
{
// TODO handle error // TODO handle error
throw new RuntimeException("Could not find main display"); throw new RuntimeException("Could not find main display");
} }
ActivityOptions options = ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(mainDisplayId);
ActivityOptions.makeBasic().setLaunchDisplayId(mainDisplayId); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
intent.setFlags( Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(EmulationActivity.EXTRA_SELECTED_GAME, gamePath); intent.putExtra(EmulationActivity.EXTRA_SELECTED_GAME, gamePath);
intent.putExtra(EmulationActivity.EXTRA_SELECTED_TITLE, gameTitle); intent.putExtra(EmulationActivity.EXTRA_SELECTED_TITLE, gameTitle);
if (context instanceof ContextWrapper) if (context instanceof ContextWrapper) {
{
ContextWrapper contextWrapper = (ContextWrapper)context; ContextWrapper contextWrapper = (ContextWrapper)context;
Context baseContext = contextWrapper.getBaseContext(); Context baseContext = contextWrapper.getBaseContext();
baseContext.startActivity(intent, options.toBundle()); baseContext.startActivity(intent, options.toBundle());
} else {
context.startActivity(intent, options.toBundle());
} }
else { context.startActivity(intent, options.toBundle()); }
((Activity)(context)).finish(); ((Activity)(context)).finish();
} }
@Override protected void onCreate(Bundle savedInstanceState) @Override
{ protected void onCreate(Bundle savedInstanceState) {
if (hasRun) if (hasRun) {
{
Log.info("VRActivity already existed"); Log.info("VRActivity already existed");
finish(); finish();
} }
@ -67,35 +68,37 @@ public class VrActivity extends EmulationActivity
// TODO assert mHandle != null // TODO assert mHandle != null
} }
@Override protected void onDestroy() @Override
{ protected void onDestroy() {
Log.info("VR [Java] onDestroy"); Log.info("VR [Java] onDestroy");
currentActivity = null; currentActivity = null;
if (mHandle != 0) { nativeOnDestroy(mHandle); } if (mHandle != 0) {
nativeOnDestroy(mHandle);
}
super.onDestroy(); super.onDestroy();
} }
@Override public void onStart() @Override
{ public void onStart() {
Log.info("VR [Java] onStart"); Log.info("VR [Java] onStart");
System.gc(); System.gc();
super.onStart(); super.onStart();
} }
@Override public void onResume() @Override
{ public void onResume() {
Log.info("VR [Java] onResume"); Log.info("VR [Java] onResume");
super.onResume(); super.onResume();
} }
@Override public void onPause() @Override
{ public void onPause() {
Log.info("VR [Java] onPause"); Log.info("VR [Java] onPause");
super.onPause(); super.onPause();
} }
@Override public void onStop() @Override
{ public void onStop() {
Log.info("VR [Java] onStop"); Log.info("VR [Java] onStop");
super.onStop(); super.onStop();
} }
@ -103,97 +106,90 @@ public class VrActivity extends EmulationActivity
private native long nativeOnCreate(); private native long nativeOnCreate();
private native void nativeOnDestroy(final long handle); private native void nativeOnDestroy(final long handle);
private static int getMainDisplay(Context context) private static int getMainDisplay(Context context) {
{
final DisplayManager displayManager = final DisplayManager displayManager =
(DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
Display[] displays = displayManager.getDisplays(); Display[] displays = displayManager.getDisplays();
for (int i = 0; i < displays.length; i++) for (int i = 0; i < displays.length; i++) {
{ if (displays[i].getDisplayId() == Display.DEFAULT_DISPLAY) {
if (displays[i].getDisplayId() == Display.DEFAULT_DISPLAY)
{
return displays[i].getDisplayId(); return displays[i].getDisplayId();
} }
} }
return -1; return -1;
} }
public void finishActivity() public void finishActivity() {
{ if (!isFinishing()) {
if (!isFinishing()) { finish(); } finish();
}
} }
void forwardVRInput(final int keycode, final boolean isPressed) void forwardVRInput(final int keycode, final boolean isPressed) {
{ KeyEvent event =
KeyEvent event = new KeyEvent( new KeyEvent(isPressed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP, keycode);
isPressed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP, keycode);
event.setSource(InputDevice.SOURCE_GAMEPAD); event.setSource(InputDevice.SOURCE_GAMEPAD);
dispatchKeyEvent(event); dispatchKeyEvent(event);
} }
void forwardVRJoystick(final float x, final float y, final int joystickType) void forwardVRJoystick(final float x, final float y, final int joystickType) {
{
// dispatch joystick input as gamepad joystick input // dispatch joystick input as gamepad joystick input
NativeLibrary.onGamePadMoveEvent( NativeLibrary.onGamePadMoveEvent("Quest controller",
"Quest controller", joystickType == 0 ? NativeLibrary.ButtonType.STICK_C
joystickType == 0 ? NativeLibrary.ButtonType.STICK_C : NativeLibrary.ButtonType.STICK_LEFT,
: NativeLibrary.ButtonType.STICK_LEFT, x, -y);
x, -y);
} }
void openSettingsMenu() void openSettingsMenu() {
{
SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, ""); SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, "");
} }
public void sendClickToWindow(final float x, final float y, public void sendClickToWindow(final float x, final float y, final int motionType) {
final int motionType)
{
clickRunnable.updateState((int)x, (int)y, motionType); clickRunnable.updateState((int)x, (int)y, motionType);
runOnUiThread(clickRunnable); runOnUiThread(clickRunnable);
} }
public void pauseGame() public void pauseGame() {
{
Log.info("VR [Java] pauseGame"); Log.info("VR [Java] pauseGame");
if (NativeLibrary.IsRunning()) { NativeLibrary.PauseEmulation(); } if (NativeLibrary.IsRunning()) {
NativeLibrary.PauseEmulation();
}
} }
public void resumeGame() public void resumeGame() {
{
Log.info("VR [Java] resumeGame"); Log.info("VR [Java] resumeGame");
// this checks to make sure the emulation has started and pausing it is // this checks to make sure the emulation has started and pausing it is
// safe -- not whether it's paused/resumed // safe -- not whether it's paused/resumed
if (NativeLibrary.IsRunning()) { NativeLibrary.UnPauseEmulation(); } if (NativeLibrary.IsRunning()) {
NativeLibrary.UnPauseEmulation();
}
} }
class ClickRunnable implements Runnable class ClickRunnable implements Runnable {
{
private int xPosition; private int xPosition;
private int yPosition; private int yPosition;
private int motionType; private int motionType;
public void updateState(int x, int y, int motionType) public void updateState(int x, int y, int motionType) {
{
this.xPosition = x; this.xPosition = x;
this.yPosition = y; this.yPosition = y;
this.motionType = motionType; this.motionType = motionType;
} }
@Override public void run() @Override
{ public void run() {
switch (motionType) switch (motionType) {
{ case 0:
case 0: NativeLibrary.onTouchEvent(0, 0, false); break; NativeLibrary.onTouchEvent(0, 0, false);
case 1: break;
NativeLibrary.onTouchEvent(xPosition, yPosition, true); case 1:
break; NativeLibrary.onTouchEvent(xPosition, yPosition, true);
case 2: NativeLibrary.onTouchMoved(xPosition, yPosition); break; break;
default: case 2:
Log.error( NativeLibrary.onTouchMoved(xPosition, yPosition);
"VR [Java] sendClickToWindow: unknown motionType: " + break;
motionType); default:
break; Log.error("VR [Java] sendClickToWindow: unknown motionType: " + motionType);
break;
} }
} }
} }

View file

@ -0,0 +1,404 @@
package org.citra.citra_emu.vr;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputFilter;
import android.text.Spanned;
import android.text.TextWatcher;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.io.Serializable;
import java.util.Objects;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.R;
import org.citra.citra_emu.applets.SoftwareKeyboard;
import org.citra.citra_emu.utils.Log;
public class VrKeyboardActivity extends android.app.Activity {
private static final String EXTRA_KEYBOARD_INPUT_CONFIG =
"org.citra.citra_emu.vr.KEYBOARD_INPUT_CONFIG";
private static final String EXTRA_KEYBOARD_RESULT = "org.citra.citra_emu.vr.KEYBOARD_RESULT";
public static class Result implements Serializable {
public static enum Type { None, Positive, Neutral, Negative }
;
public Result() {
text = "";
type = Type.None;
config = null;
}
public Result(final String text, final Type type,
final SoftwareKeyboard.KeyboardConfig config) {
this.text = text;
this.type = type;
this.config = config;
}
public Result(final Type type) {
this.text = "";
this.type = type;
this.config = null;
}
public String text;
public Type type;
public SoftwareKeyboard.KeyboardConfig config;
}
public static class Contract
extends ActivityResultContract<SoftwareKeyboard.KeyboardConfig, Result> {
@Override
public Intent createIntent(Context context, final SoftwareKeyboard.KeyboardConfig config) {
Intent intent = new Intent(context, VrKeyboardActivity.class);
intent.putExtra(EXTRA_KEYBOARD_INPUT_CONFIG, config);
return intent;
}
@Override
public Result parseResult(int resultCode, Intent intent) {
if (resultCode != Activity.RESULT_OK) {
Log.warning("parseResult(): Unexpected result code: " + resultCode);
return new Result();
}
if (intent != null) {
final Result result = (Result)intent.getSerializableExtra(EXTRA_KEYBOARD_RESULT);
if (result != null) {
return result;
}
}
Log.warning("parseResult(): finished with OK, but no result. Intent: " + intent);
return new Result();
}
}
public static void onFinishResult(final Result result) {
switch (result.type) {
case Positive:
SoftwareKeyboard.onFinishVrKeyboardPositive(result.text, result.config);
break;
case Neutral:
SoftwareKeyboard.onFinishVrKeyboardNeutral();
break;
case Negative:
case None:
SoftwareKeyboard.onFinishVrKeyboardNegative();
break;
}
}
private static enum KeyboardType { None, Abc, Num }
private EditText mEditText = null;
private boolean mIsShifted = false;
private KeyboardType mKeyboardTypeCur = KeyboardType.None;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle extras = getIntent().getExtras();
SoftwareKeyboard.KeyboardConfig config = new SoftwareKeyboard.KeyboardConfig();
if (extras != null) {
config = (SoftwareKeyboard.KeyboardConfig)extras.getSerializable(
EXTRA_KEYBOARD_INPUT_CONFIG);
}
setContentView(R.layout.vr_keyboard);
mEditText = findViewById(R.id.vrKeyboardText);
{
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.leftMargin = params.rightMargin =
CitraApplication.getAppContext().getResources().getDimensionPixelSize(
R.dimen.dialog_margin);
mEditText.setHint(config.hint_text);
mEditText.setSingleLine(!config.multiline_mode);
mEditText.setLayoutParams(params);
mEditText.setFilters(
new InputFilter[] {new SoftwareKeyboard.Filter(),
new InputFilter.LengthFilter(config.max_text_length)});
}
// Needed to show cursor onscreen.
mEditText.requestFocus();
WindowCompat.getInsetsController(getWindow(), mEditText)
.show(WindowInsetsCompat.Type.ime());
setupResultButtons(config);
showKeyboardType(KeyboardType.Abc);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (!hasFocus) {
finish(); // Finish the activity when it loses focus, like an AlertDialog.
}
}
private void setupResultButtons(final SoftwareKeyboard.KeyboardConfig config) {
// Configure the result buttons
findViewById(R.id.keyPositive).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
Intent resultIntent = new Intent();
resultIntent.putExtra(
EXTRA_KEYBOARD_RESULT,
new Result(mEditText.getText().toString(), Result.Type.Positive, config));
setResult(Activity.RESULT_OK, resultIntent);
finish();
}
return false;
}
});
findViewById(R.id.keyNeutral).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
Intent resultIntent = new Intent();
resultIntent.putExtra(EXTRA_KEYBOARD_RESULT, new Result(Result.Type.Neutral));
setResult(Activity.RESULT_OK, resultIntent);
finish();
}
return false;
}
});
findViewById(R.id.keyNegative).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
Intent resultIntent = new Intent();
resultIntent.putExtra(EXTRA_KEYBOARD_RESULT, new Result(Result.Type.Negative));
setResult(Activity.RESULT_OK, resultIntent);
finish();
}
return false;
}
});
switch (config.button_config) {
case SoftwareKeyboard.ButtonConfig.Triple:
findViewById(R.id.keyNeutral).setVisibility(View.VISIBLE);
// fallthrough
case SoftwareKeyboard.ButtonConfig.Dual:
findViewById(R.id.keyNegative).setVisibility(View.VISIBLE);
// fallthrough
case SoftwareKeyboard.ButtonConfig.Single:
findViewById(R.id.keyPositive).setVisibility(View.VISIBLE);
// fallthrough
case SoftwareKeyboard.ButtonConfig.None:
break;
default:
Log.error("Unknown button config: " + config.button_config);
assert false;
}
}
private void showKeyboardType(final KeyboardType keyboardType) {
if (mKeyboardTypeCur == keyboardType) {
return;
}
mKeyboardTypeCur = keyboardType;
final ViewGroup keyboard = findViewById(R.id.vr_keyboard_keyboard);
keyboard.removeAllViews();
switch (keyboardType) {
case Abc:
getLayoutInflater().inflate(R.layout.vr_keyboard_abc, keyboard);
addLetterKeyHandlersForViewGroup(keyboard, mIsShifted);
break;
case Num:
getLayoutInflater().inflate(R.layout.vr_keyboard_123, keyboard);
addLetterKeyHandlersForViewGroup(keyboard, false);
break;
default:
assert false;
}
addModifierKeyHandlers();
}
private void addModifierKeyHandlers() {
findViewById(R.id.keyShift).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
setKeyCase(!mIsShifted);
}
return false;
}
});
// Note: I prefer touch listeners over click listeners because they activate
// on the press instead of the release and therefore feel more responsive.
findViewById(R.id.keyBackspace).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
final String text = mEditText.getText().toString();
if (text.length() > 0) {
// Delete character before cursor
final int position = mEditText.getSelectionStart();
if (position > 0) {
final String newText =
text.substring(0, position - 1) + text.substring(position);
mEditText.setText(newText);
mEditText.setSelection(position - 1);
}
}
}
return false;
}
});
findViewById(R.id.keySpace).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
final int position = mEditText.getSelectionStart();
if (position < mEditText.getText().length()) {
final String newText =
mEditText.getText().toString().substring(0, position) + " " +
mEditText.getText().toString().substring(position);
mEditText.setText(newText);
mEditText.setSelection(position + 1);
} else {
mEditText.append(" ");
}
}
return false;
}
});
findViewById(R.id.keyLeft).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
final int position = mEditText.getSelectionStart();
if (position > 0) {
mEditText.setSelection(position - 1);
}
}
return false;
}
});
findViewById(R.id.keyRight).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
final int position = mEditText.getSelectionStart();
if (position < mEditText.getText().length()) {
mEditText.setSelection(position + 1);
}
}
return false;
}
});
if (findViewById(R.id.keyNumbers) != null) {
findViewById(R.id.keyNumbers).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
showKeyboardType(KeyboardType.Num);
}
return false;
}
});
}
if (findViewById(R.id.keyAbc) != null) {
findViewById(R.id.keyAbc).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
showKeyboardType(KeyboardType.Abc);
}
return false;
}
});
}
}
private void addLetterKeyHandlersForViewGroup(final ViewGroup viewGroup,
final boolean isShifted) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
final View child = viewGroup.getChildAt(i);
if (child instanceof ViewGroup) {
addLetterKeyHandlersForViewGroup((ViewGroup)child, isShifted);
} else if (child instanceof Button) {
if ("key_letter".equals(child.getTag())) {
final Button key = (Button)child;
key.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
final int position = mEditText.getSelectionStart();
if (position < mEditText.getText().length()) {
final String newText =
mEditText.getText().toString().substring(0, position) +
key.getText().toString() +
mEditText.getText().toString().substring(position);
mEditText.setText(newText);
mEditText.setSelection(position + 1);
} else {
mEditText.append(key.getText().toString());
}
}
return false;
}
});
setKeyCaseForButton(key, isShifted);
}
}
}
}
private void setKeyCase(final boolean isShifted) {
mIsShifted = isShifted;
final ViewGroup layout = findViewById(R.id.vr_keyboard);
setKeyCaseForViewGroup(layout, isShifted);
}
private static void setKeyCaseForViewGroup(ViewGroup viewGroup, final boolean isShifted) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
final View child = viewGroup.getChildAt(i);
if (child instanceof ViewGroup) {
setKeyCaseForViewGroup((ViewGroup)child, isShifted);
} else if (child instanceof Button && "key_letter".equals(child.getTag())) {
setKeyCaseForButton((Button)child, isShifted);
}
}
}
private static void setKeyCaseForButton(Button button, final boolean isShifted) {
final String text = button.getText().toString();
if (isShifted) {
button.setText(text.toUpperCase());
} else {
button.setText(text.toLowerCase());
}
}
}

View file

@ -0,0 +1,16 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Pressed state -->
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="?attr/colorSecondary"/> <!-- Pressed color -->
<corners android:radius="4dp"/>
</shape>
</item>
<!-- Default state -->
<item>
<shape android:shape="rectangle">
<solid android:color="?attr/colorPrimary"/> <!-- Default color -->
<corners android:radius="4dp"/>
</shape>
</item>
</selector>

View file

@ -0,0 +1,74 @@
<!--FML for having to write this f$%king keyboard.
I would not have had to do this were it not for a bug/idiosyncrasy
in Quest's window manager that returns the wrong value for "is this window in
focus?" when the packagename of the foregrounded window content is the same as
the backgrounded immersive window (even when the two activities/windows are in
different processes).
This bug prevents me from pulling up the Quest system keyboard.
Why it happens: Quest's window manager is performing a single check based on the
app's packagename and returns information for the wrong window.-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
android:theme="@style/Theme.Citra.Main"
android:paddingTop="40dp"
android:id="@+id/vr_keyboard"
android:layout_marginHorizontal="30dp"
>
<EditText
android:id="@+id/vrKeyboardText"
style="@style/VrKeyboardEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="text"
android:focusable="true"
android:focusableInTouchMode="true"/>
<LinearLayout
android:id="@+id/vr_keyboard_keyboard"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/keyPositive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="gone"
android:text="@android:string/ok" />
<Button
android:id="@+id/keyNeutral"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="gone"
android:text="@string/i_forgot" />
<Button
android:id="@+id/keyNegative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="gone"
android:text="@android:string/cancel" />
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,74 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.Citra.Main"
android:orientation="vertical"
android:padding="16dp">
<!-- Row for numbers -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button android:id="@+id/key1" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="1"/>
<Button android:id="@+id/key2" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="2"/>
<Button android:id="@+id/key3" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="3"/>
<Button android:id="@+id/key4" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="4"/>
<Button android:id="@+id/key5" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="5"/>
<Button android:id="@+id/key6" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="6"/>
<Button android:id="@+id/key7" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="7"/>
<Button android:id="@+id/key8" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="8"/>
<Button android:id="@+id/key9" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="9"/>
<Button android:id="@+id/key0" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:text="0"/>
</LinearLayout>
<!-- Row for symbols -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button android:id="@+id/keyAt" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:text="@"/>
<Button android:id="@+id/keyHash" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:text="#"/>
<Button android:id="@+id/keyDollar" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:text="$"/>
<Button android:id="@+id/keyPercent" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:text="%"/>
<Button android:id="@+id/keyAmpersand" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:text="&amp;"/>
<Button android:id="@+id/keyStar" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:text="*"/>
<Button android:id="@+id/keyMinus" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:text="-"/>
<Button android:id="@+id/keyPlus" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:text="+"/>
<Button android:id="@+id/keyLParen" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="("/>
<Button android:id="@+id/keyRParen" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text=")"/>
</LinearLayout>
<!-- Row for more symbols and actions like delete or enter -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button android:id="@+id/keyShift" android:tag="key_modifier" style="@style/VrKeyboardButtonStyleModifier" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1.5" android:text="Shift"/>
<Button android:id="@+id/keyExclamation" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="!"/>
<Button android:id="@+id/keyQuote" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="&quot;"/>
<Button android:id="@+id/keyApostrophe" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="'"/>
<Button android:id="@+id/keyColon" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text=":"/>
<Button android:id="@+id/keySemicolon" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text=";"/>
<Button android:id="@+id/keySlash" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="/"/>
<Button android:id="@+id/keyQuestionMark" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="?"/>
<Button android:id="@+id/keyBackspace" android:tag="key_modifier" style="@style/VrKeyboardButtonStyleModifier" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1.5" android:text="Del"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button android:id="@+id/keyAbc" android:tag="key_modifier" style="@style/VrKeyboardButtonStyleModifier" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:text="abc"/>
<Button android:id="@+id/keySpace" android:tag="key_modifier" style="@style/VrKeyboardButtonStyleModifier" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="4" android:text="Space"/>
<Button android:id="@+id/keyLeft" android:tag="key_modifier" style="@style/VrKeyboardButtonStyleModifier" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="&lt;"/>
<Button android:id="@+id/keyRight" android:tag="key_modifier" style="@style/VrKeyboardButtonStyleModifier" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="&gt;"/>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,68 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:theme="@style/Theme.Citra.Main"
android:padding="16dp">
<!-- Row 1 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button android:id="@+id/keyQ" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="q"/>
<Button android:id="@+id/keyW" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="w"/>
<Button android:id="@+id/keyE" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="e"/>
<Button android:id="@+id/keyR" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="r"/>
<Button android:id="@+id/keyT" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="t"/>
<Button android:id="@+id/keyY" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="y"/>
<Button android:id="@+id/keyU" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="u"/>
<Button android:id="@+id/keyI" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="i"/>
<Button android:id="@+id/keyO" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="o"/>
<Button android:id="@+id/keyP" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="p"/>
</LinearLayout>
<!-- Row 2 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<Button android:id="@+id/keyA" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="a"/>
<Button android:id="@+id/keyS" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="s"/>
<Button android:id="@+id/keyD" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="d"/>
<Button android:id="@+id/keyF" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="f"/>
<Button android:id="@+id/keyG" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="g"/>
<Button android:id="@+id/keyH" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="h"/>
<Button android:id="@+id/keyJ" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="J"/>
<Button android:id="@+id/keyK" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="k"/>
<Button android:id="@+id/keyL" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="l"/>
</LinearLayout>
<!-- Row 3 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button android:id="@+id/keyShift" android:tag="key_modifier" style="@style/VrKeyboardButtonStyleModifier" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1.5" android:text="Shift"/>
<Button android:id="@+id/keyZ" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="z"/>
<Button android:id="@+id/keyX" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="x"/>
<Button android:id="@+id/keyC" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="c"/>
<Button android:id="@+id/keyV" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="v"/>
<Button android:id="@+id/keyB" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="b"/>
<Button android:id="@+id/keyN" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="n"/>
<Button android:id="@+id/keyM" android:tag="key_letter" style="@style/VrKeyboardButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="m"/>
<Button android:id="@+id/keyBackspace" android:tag="key_modifier" style="@style/VrKeyboardButtonStyleModifier" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1.5" android:text="Del"/>
</LinearLayout>
<!-- Row 4 (Space, Enter, etc.) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button android:id="@+id/keyNumbers" android:tag="key_modifier" style="@style/VrKeyboardButtonStyleModifier" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:text="123"/>
<Button android:id="@+id/keySpace" android:tag="key_modifier" style="@style/VrKeyboardButtonStyleModifier" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="4" android:text="Space"/>
<Button android:id="@+id/keyLeft" android:tag="key_modifier" style="@style/VrKeyboardButtonStyleModifier" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="&lt;"/>
<Button android:id="@+id/keyRight" android:tag="key_modifier" style="@style/VrKeyboardButtonStyleModifier" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="&gt;"/>
</LinearLayout>
</LinearLayout>

View file

@ -290,7 +290,8 @@
<!-- VR-specific --> <!-- VR-specific -->
<string name="vr_gamepad_warning">Having trouble? Press @string/button_start + @string/button_select on the gamepad to toggle input modes</string> <string name="vr_gamepad_warning">Having trouble? Press @string/button_start + @string/button_select on the gamepad to toggle input modes</string>
<string name="preferences_vr">VR</string> <string name="preferences_vr">VR</string>
<string name="vr_background">VR Environment</string> <string name="vr_background">VR Environment</string>
<string name="vr_keyboard_left">&lt;</string>
<string name="vr_keyboard_right">&gt;</string>
</resources> </resources>

View file

@ -51,4 +51,26 @@
</style> </style>
<!-- Style for keyboard buttons -->
<style name="VrKeyboardButtonStyle" parent="Widget.Material3.Button">
<item name="android:background">@drawable/vr_keyboard_key_background</item>
<item name="android:textSize">18sp</item>
<item name="android:textColor">@android:color/white</item>
<item name="android:padding">10dp</item>
<item name="android:layout_margin">2dp</item>
<item name="android:textAllCaps">false</item>
<item name="colorPrimary">@color/citra_primary</item>
</style>
<style name="VrKeyboardButtonStyleModifier" parent="@style/VrKeyboardButtonStyle">
<item name="colorPrimary">@color/citra_secondary</item>
</style>
<style name="VrKeyboardEditText" parent="Widget.AppCompat.EditText">
<item name="android:textColor">@android:color/white</item>
<item name="android:textSize">16sp</item>
<item name="android:padding">12dp</item>
</style>
</resources> </resources>