From c90d0d27de7c3dd803d23b14cefa378f3f160a32 Mon Sep 17 00:00:00 2001 From: amwatson Date: Thu, 18 Jan 2024 15:48:25 -0600 Subject: [PATCH] [VrKeyboardActivity] Add a hacky in-app keyboard that won't crash --- src/android/app/src/main/AndroidManifest.xml | 16 + .../citra_emu/applets/SoftwareKeyboard.java | 169 +++++--- .../citra/citra_emu/vr/ErrorMessageLayer.java | 13 +- .../citra/citra_emu/vr/GameSurfaceLayer.java | 10 +- .../org/citra/citra_emu/vr/VrActivity.java | 158 ++++--- .../citra_emu/vr/VrKeyboardActivity.java | 404 ++++++++++++++++++ .../drawable/vr_keyboard_key_background.xml | 16 + .../app/src/main/res/layout/vr_keyboard.xml | 74 ++++ .../src/main/res/layout/vr_keyboard_123.xml | 74 ++++ .../src/main/res/layout/vr_keyboard_abc.xml | 68 +++ .../app/src/main/res/values/strings.xml | 3 +- .../app/src/main/res/values/styles.xml | 22 + 12 files changed, 865 insertions(+), 162 deletions(-) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/vr/VrKeyboardActivity.java create mode 100644 src/android/app/src/main/res/drawable/vr_keyboard_key_background.xml create mode 100644 src/android/app/src/main/res/layout/vr_keyboard.xml create mode 100644 src/android/app/src/main/res/layout/vr_keyboard_123.xml create mode 100644 src/android/app/src/main/res/layout/vr_keyboard_abc.xml diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 6777781ca..1bdf02513 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -126,6 +126,22 @@ + + + + + + + + + + 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) { try { @@ -256,11 +297,11 @@ public static KeyboardData Execute(KeyboardConfig config) { public static void ShowError(String error) { NativeLibrary.displayAlertMsg( - CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard), - error, false); + CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard), + 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); } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/vr/ErrorMessageLayer.java b/src/android/app/src/main/java/org/citra/citra_emu/vr/ErrorMessageLayer.java index 0abc14b78..2123b2277 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/vr/ErrorMessageLayer.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/vr/ErrorMessageLayer.java @@ -1,19 +1,12 @@ package org.citra.citra_emu.vr; -public class ErrorMessageLayer -{ +public class ErrorMessageLayer { public static ErrorMessageLayer instance = null; - public static void showErrorWindow(final String titleStr, - final String mainMessageStr) - { - } + public static void showErrorWindow(final String titleStr, final String mainMessageStr) {} - public void _showErrorWindow(final String titleStr, - final String mainMessageStr) - { - } + public void _showErrorWindow(final String titleStr, final String mainMessageStr) {} public void hideErrorWindow() {} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/vr/GameSurfaceLayer.java b/src/android/app/src/main/java/org/citra/citra_emu/vr/GameSurfaceLayer.java index fbdede56d..176428802 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/vr/GameSurfaceLayer.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/vr/GameSurfaceLayer.java @@ -14,13 +14,11 @@ * Note: this is set up to require the min number of changes possible to *existing Citra code, in case an upstream merge is desired. **/ -public class GameSurfaceLayer -{ - public static void setSurface(VrActivity activity, Surface surface) - { +public class GameSurfaceLayer { + public static void setSurface(VrActivity activity, Surface surface) { assert activity != null; - ((EmulationFragment)activity.getSupportFragmentManager() - .findFragmentById(R.id.frame_emulation_fragment)) + ((EmulationFragment)activity.getSupportFragmentManager().findFragmentById( + R.id.frame_emulation_fragment)) .surfaceCreated(surface); } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/vr/VrActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/vr/VrActivity.java index a8693992d..682ebd8dd 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/vr/VrActivity.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/vr/VrActivity.java @@ -10,52 +10,53 @@ import android.view.Display; import android.view.InputDevice; import android.view.KeyEvent; +import androidx.activity.result.ActivityResultLauncher; import org.citra.citra_emu.NativeLibrary; 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.utils.SettingsFile; import org.citra.citra_emu.utils.Log; - -public class VrActivity extends EmulationActivity -{ +public class VrActivity extends EmulationActivity { private long mHandle = 0; public static boolean hasRun = false; public static VrActivity currentActivity = null; ClickRunnable clickRunnable = new ClickRunnable(); - static { System.loadLibrary("openxr_forwardloader.oculus"); } - public static void launch(Context context, final String gamePath, - final String gameTitle) - { + static { + System.loadLibrary("openxr_forwardloader.oculus"); + } + + public final ActivityResultLauncher 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); final int mainDisplayId = getMainDisplay(context); - if (mainDisplayId < 0) - { + if (mainDisplayId < 0) { // TODO handle error throw new RuntimeException("Could not find main display"); } - ActivityOptions options = - ActivityOptions.makeBasic().setLaunchDisplayId(mainDisplayId); - intent.setFlags( - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | - Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(mainDisplayId); + intent.setFlags(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_TITLE, gameTitle); - if (context instanceof ContextWrapper) - { + if (context instanceof ContextWrapper) { ContextWrapper contextWrapper = (ContextWrapper)context; Context baseContext = contextWrapper.getBaseContext(); baseContext.startActivity(intent, options.toBundle()); + } else { + context.startActivity(intent, options.toBundle()); } - else { context.startActivity(intent, options.toBundle()); } ((Activity)(context)).finish(); } - @Override protected void onCreate(Bundle savedInstanceState) - { - if (hasRun) - { + @Override + protected void onCreate(Bundle savedInstanceState) { + if (hasRun) { Log.info("VRActivity already existed"); finish(); } @@ -67,35 +68,37 @@ public static void launch(Context context, final String gamePath, // TODO assert mHandle != null } - @Override protected void onDestroy() - { + @Override + protected void onDestroy() { Log.info("VR [Java] onDestroy"); currentActivity = null; - if (mHandle != 0) { nativeOnDestroy(mHandle); } + if (mHandle != 0) { + nativeOnDestroy(mHandle); + } super.onDestroy(); } - @Override public void onStart() - { + @Override + public void onStart() { Log.info("VR [Java] onStart"); System.gc(); super.onStart(); } - @Override public void onResume() - { + @Override + public void onResume() { Log.info("VR [Java] onResume"); super.onResume(); } - @Override public void onPause() - { + @Override + public void onPause() { Log.info("VR [Java] onPause"); super.onPause(); } - @Override public void onStop() - { + @Override + public void onStop() { Log.info("VR [Java] onStop"); super.onStop(); } @@ -103,97 +106,90 @@ public static void launch(Context context, final String gamePath, private native long nativeOnCreate(); private native void nativeOnDestroy(final long handle); - private static int getMainDisplay(Context context) - { + private static int getMainDisplay(Context context) { final DisplayManager displayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); Display[] displays = displayManager.getDisplays(); - for (int i = 0; i < displays.length; i++) - { - if (displays[i].getDisplayId() == Display.DEFAULT_DISPLAY) - { + for (int i = 0; i < displays.length; i++) { + if (displays[i].getDisplayId() == Display.DEFAULT_DISPLAY) { return displays[i].getDisplayId(); } } return -1; } - public void finishActivity() - { - if (!isFinishing()) { finish(); } + public void finishActivity() { + if (!isFinishing()) { + finish(); + } } - void forwardVRInput(final int keycode, final boolean isPressed) - { - KeyEvent event = new KeyEvent( - isPressed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP, keycode); + void forwardVRInput(final int keycode, final boolean isPressed) { + KeyEvent event = + new KeyEvent(isPressed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP, keycode); event.setSource(InputDevice.SOURCE_GAMEPAD); 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 - NativeLibrary.onGamePadMoveEvent( - "Quest controller", - joystickType == 0 ? NativeLibrary.ButtonType.STICK_C - : NativeLibrary.ButtonType.STICK_LEFT, - x, -y); + NativeLibrary.onGamePadMoveEvent("Quest controller", + joystickType == 0 ? NativeLibrary.ButtonType.STICK_C + : NativeLibrary.ButtonType.STICK_LEFT, + x, -y); } - void openSettingsMenu() - { + void openSettingsMenu() { SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, ""); } - public void sendClickToWindow(final float x, final float y, - final int motionType) - { + public void sendClickToWindow(final float x, final float y, final int motionType) { clickRunnable.updateState((int)x, (int)y, motionType); runOnUiThread(clickRunnable); } - public void pauseGame() - { + public void 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"); // this checks to make sure the emulation has started and pausing it is // 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 yPosition; 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.yPosition = y; this.motionType = motionType; } - @Override public void run() - { - switch (motionType) - { - case 0: NativeLibrary.onTouchEvent(0, 0, false); break; - case 1: - NativeLibrary.onTouchEvent(xPosition, yPosition, true); - break; - case 2: NativeLibrary.onTouchMoved(xPosition, yPosition); break; - default: - Log.error( - "VR [Java] sendClickToWindow: unknown motionType: " + - motionType); - break; + @Override + public void run() { + switch (motionType) { + case 0: + NativeLibrary.onTouchEvent(0, 0, false); + break; + case 1: + NativeLibrary.onTouchEvent(xPosition, yPosition, true); + break; + case 2: + NativeLibrary.onTouchMoved(xPosition, yPosition); + break; + default: + Log.error("VR [Java] sendClickToWindow: unknown motionType: " + motionType); + break; } } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/vr/VrKeyboardActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/vr/VrKeyboardActivity.java new file mode 100644 index 000000000..00aa880f5 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/vr/VrKeyboardActivity.java @@ -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 { + @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()); + } + } +} diff --git a/src/android/app/src/main/res/drawable/vr_keyboard_key_background.xml b/src/android/app/src/main/res/drawable/vr_keyboard_key_background.xml new file mode 100644 index 000000000..5b0700394 --- /dev/null +++ b/src/android/app/src/main/res/drawable/vr_keyboard_key_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/layout/vr_keyboard.xml b/src/android/app/src/main/res/layout/vr_keyboard.xml new file mode 100644 index 000000000..78475d1ee --- /dev/null +++ b/src/android/app/src/main/res/layout/vr_keyboard.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + +