diff --git a/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp b/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp index 745d30d9..7ff2bed0 100644 --- a/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp +++ b/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp @@ -983,7 +983,9 @@ void LatteRenderTarget_copyToBackbuffer(LatteTextureView* textureView, bool isPa g_renderer->HandleScreenshotRequest(textureView, isPadView); if (!g_renderer->ImguiBegin(!isPadView)) return; - swkbd_render(!isPadView); +#if !__ANDROID__ + swkbd::render(!isPadView); +#endif nn::erreula::render(!isPadView); LatteOverlay_render(isPadView); g_renderer->ImguiEnd(); @@ -1003,7 +1005,7 @@ void LatteRenderTarget_itHLECopyColorBufferToScanBuffer(MPTR colorBufferPtr, uin const bool tabPressed = GuiSystem::isKeyDown(GuiSystem::PlatformKeyCodes::TAB); const bool ctrlPressed = GuiSystem::isKeyDown(GuiSystem::PlatformKeyCodes::LCONTROL); - bool showDRC = swkbd_hasKeyboardInputHook() == false && tabPressed; + bool showDRC = swkbd::hasKeyboardInputHook() == false && tabPressed; bool& alwaysDisplayDRC = LatteGPUState.alwaysDisplayDRC; if (ctrlPressed && tabPressed) diff --git a/src/Cafe/OS/libs/swkbd/swkbd.cpp b/src/Cafe/OS/libs/swkbd/swkbd.cpp index d30992b0..dad253d4 100644 --- a/src/Cafe/OS/libs/swkbd/swkbd.cpp +++ b/src/Cafe/OS/libs/swkbd/swkbd.cpp @@ -1,3 +1,4 @@ +#include "OS/libs/swkbd/swkbd.h" #include "Cafe/OS/common/OSCommon.h" #include "Cafe/HW/Latte/ISA/RegDefines.h" #include "Cafe/OS/libs/gx2/GX2.h" @@ -18,7 +19,7 @@ #define SWKBD_STATE_DISPLAYED (2) // visible #define SWKBD_STATE_DISAPPEARING (3) // fade-out ? -typedef struct +typedef struct { uint32 ukn00; // constructor? uint32 ukn04; // destructor? @@ -26,7 +27,7 @@ typedef struct MEMPTR changeString; // some function address }SwkbdIEventReceiverVTable_t; -typedef struct +typedef struct { MEMPTR vTable; // todo - more elements? (currently separated from this struct) @@ -42,7 +43,7 @@ struct swkbdReceiverArg_t sint32be selectFrom; }; -typedef struct +typedef struct { uint32 ukn000; uint32 controllerType; @@ -67,7 +68,7 @@ typedef struct swkbdReceiverArg_t receiverArg; }SwkbdKeyboardArg_t; -typedef struct +typedef struct { // this structure resides in PPC addressable memory space wchar_t formStringBuffer[SWKBD_FORM_STRING_MAX_LENGTH]; @@ -86,11 +87,18 @@ typedef struct bool shiftActivated; bool returnState; bool cancelState; - + }swkbdInternalState_t; swkbdInternalState_t* swkbdInternalState = NULL; +std::shared_ptr s_swkbdCallbacks; + +void swkbd::setSwkbdCallbacks(const std::shared_ptr& swkbdCallbacks) +{ + s_swkbdCallbacks = swkbdCallbacks; +} + void swkbdExport_SwkbdCreate(PPCInterpreter_t* hCPU) { cemuLog_logDebug(LogType::Force, "swkbd.SwkbdCreate(0x{:08x},0x{:08x},0x{:08x},0x{:08x})", hCPU->gpr[3], hCPU->gpr[4], hCPU->gpr[5], hCPU->gpr[6]); @@ -142,12 +150,12 @@ void swkbdExport_SwkbdGetStateInputForm(PPCInterpreter_t* hCPU) // ? -typedef struct +typedef struct { MPTR vTable; // guessed }swkdbIEventReceiver_t; -typedef struct +typedef struct { uint32 ukn00; uint32 ukn04; @@ -171,7 +179,7 @@ void swkbdExport_SwkbdSetReceiver(PPCInterpreter_t* hCPU) osLib_returnFromFunction(hCPU, 0); } -typedef struct +typedef struct { /* +0x00 */ uint32 ukn00; /* +0x04 */ uint32 ukn04; @@ -235,6 +243,9 @@ void swkbdExport_SwkbdAppearInputForm(PPCInterpreter_t* hCPU) swkbdInternalState->formStringBuffer[0] = '\0'; swkbdInternalState->formStringLength = 0; } + if (s_swkbdCallbacks) + s_swkbdCallbacks->showSoftwareKeyboard({swkbdInternalState->formStringBuffer, swkbdInternalState->formStringBuffer + swkbdInternalState->formStringLength}, swkbdInternalState->maxTextLength); + osLib_returnFromFunction(hCPU, 1); } @@ -257,6 +268,11 @@ void swkbdExport_SwkbdAppearKeyboard(PPCInterpreter_t* hCPU) swkbdInternalState->formStringBuffer[0] = '\0'; swkbdInternalState->formStringLength = 0; swkbdInternalState->keyboardArg = *keyboardArg; + if (s_swkbdCallbacks) + { + sint32 maxLength = std::max(swkbdInternalState->keyboardArg.receiverArg.stringBufSize - 1, 0); + s_swkbdCallbacks->showSoftwareKeyboard({swkbdInternalState->formStringBuffer, swkbdInternalState->formStringBuffer + swkbdInternalState->formStringLength}, maxLength); + } osLib_returnFromFunction(hCPU, 1); } @@ -264,6 +280,8 @@ void swkbdExport_SwkbdDisappearInputForm(PPCInterpreter_t* hCPU) { debug_printf("SwkbdDisappearInputForm__3RplFv LR: %08x\n", hCPU->spr.LR); swkbdInternalState->isActive = false; + if (s_swkbdCallbacks) + s_swkbdCallbacks->hideSoftwareKeyboard(); osLib_returnFromFunction(hCPU, 1); } @@ -271,6 +289,8 @@ void swkbdExport_SwkbdDisappearKeyboard(PPCInterpreter_t* hCPU) { debug_printf("SwkbdDisappearKeyboard__3RplFv LR: %08x\n", hCPU->spr.LR); swkbdInternalState->isActive = false; + if (s_swkbdCallbacks) + s_swkbdCallbacks->hideSoftwareKeyboard(); osLib_returnFromFunction(hCPU, 1); } @@ -292,7 +312,7 @@ void swkbdExport_SwkbdIsDecideOkButton(PPCInterpreter_t* hCPU) osLib_returnFromFunction(hCPU, 0); } -typedef struct +typedef struct { uint32be ukn00; uint32be ukn04; @@ -349,13 +369,12 @@ void swkbdExport_SwkbdIsNeedCalcSubThreadPredict(PPCInterpreter_t* hCPU) osLib_returnFromFunction(hCPU, r?1:0); } -void swkbd_keyInput(uint32 keyCode); -void swkbd_render(bool mainWindow) +void swkbd::render(bool mainWindow) { // only render if active if( swkbdInternalState == NULL || swkbdInternalState->isActive == false) return; - + auto& io = ImGui::GetIO(); const auto font = ImGui_GetFont(48.0f); const auto textFont = ImGui_GetFont(24.0f); @@ -438,13 +457,13 @@ void swkbd_render(bool mainWindow) if (ImGui::Button(key, { *key == ' ' ? 537 : (button_len + 5), 0})) { if (strcmp(key, _utf8WrapperPtr(ICON_FA_ARROW_CIRCLE_LEFT)) == 0) - swkbd_keyInput(8); + swkbd::keyInput(BACKSPACE_KEYCODE); else if (strcmp(key, _utf8WrapperPtr(ICON_FA_ARROW_UP)) == 0) swkbdInternalState->shiftActivated = !swkbdInternalState->shiftActivated; else if (strcmp(key, _utf8WrapperPtr(ICON_FA_CHECK)) == 0) - swkbd_keyInput(13); + swkbd::keyInput(RETURN_KEYCODE); else - swkbd_keyInput(*key); + swkbd::keyInput(*key); } ImGui::SameLine(); @@ -470,13 +489,13 @@ void swkbd_render(bool mainWindow) if (ImGui::Button(key, { *key == ' ' ? 537 : (button_len + 5), 0 })) { if (strcmp(key, _utf8WrapperPtr(ICON_FA_ARROW_CIRCLE_LEFT)) == 0) - swkbd_keyInput(8); + swkbd::keyInput(BACKSPACE_KEYCODE); else if (strcmp(key, _utf8WrapperPtr(ICON_FA_ARROW_UP)) == 0) swkbdInternalState->shiftActivated = !swkbdInternalState->shiftActivated; else if (strcmp(key, _utf8WrapperPtr(ICON_FA_CHECK)) == 0) - swkbd_keyInput(13); + swkbd::keyInput(RETURN_KEYCODE); else - swkbd_keyInput(*key); + swkbd::keyInput(*key); } ImGui::SameLine(); @@ -492,7 +511,7 @@ void swkbd_render(bool mainWindow) if (io.NavInputs[ImGuiNavInput_Cancel] > 0) { if(!swkbdInternalState->cancelState) - swkbd_keyInput(8); // backspace + swkbd::keyInput(BACKSPACE_KEYCODE); swkbdInternalState->cancelState = true; } else @@ -501,7 +520,7 @@ void swkbd_render(bool mainWindow) if (io.NavInputs[ImGuiNavInput_Input] > 0) { if (!swkbdInternalState->returnState) - swkbd_keyInput(13); // return + swkbd::keyInput(RETURN_KEYCODE); swkbdInternalState->returnState = true; } else @@ -511,7 +530,7 @@ void swkbd_render(bool mainWindow) ImGui::PopStyleColor(); } -bool swkbd_hasKeyboardInputHook() +bool swkbd::hasKeyboardInputHook() { return swkbdInternalState != NULL && swkbdInternalState->isActive; } @@ -521,7 +540,7 @@ void swkbd_finishInput() swkbdInternalState->decideButtonWasPressed = true; // currently we always accept the input } -typedef struct +typedef struct { uint32be beginIndex; uint32be endIndex; @@ -564,9 +583,9 @@ void swkbd_inputStringChanged() } } -void swkbd_keyInput(uint32 keyCode) +void swkbd::keyInput(uint32 keyCode) { - if (keyCode == 8 || keyCode == 127) // backspace || backwards delete + if (keyCode == BACKSPACE_KEYCODE || keyCode == BACKWARDS_DELETE_KEYCODE) { if (swkbdInternalState->formStringLength > 0) swkbdInternalState->formStringLength--; @@ -574,7 +593,7 @@ void swkbd_keyInput(uint32 keyCode) swkbd_inputStringChanged(); return; } - else if (keyCode == 13) // return + else if (keyCode == RETURN_KEYCODE) { swkbd_finishInput(); return; @@ -593,7 +612,7 @@ void swkbd_keyInput(uint32 keyCode) { // allowed } - else if (keyCode < 32) + else if (keyCode < CONTROL_KEYCODE) { // control key return; diff --git a/src/Cafe/OS/libs/swkbd/swkbd.h b/src/Cafe/OS/libs/swkbd/swkbd.h index 2456b25a..69f37e1c 100644 --- a/src/Cafe/OS/libs/swkbd/swkbd.h +++ b/src/Cafe/OS/libs/swkbd/swkbd.h @@ -1,8 +1,24 @@ -void swkbd_render(bool mainWindow); -bool swkbd_hasKeyboardInputHook(); -void swkbd_keyInput(uint32 keyCode); +#pragma once namespace swkbd { + constexpr uint32 BACKSPACE_KEYCODE = 8; + constexpr uint32 BACKWARDS_DELETE_KEYCODE = 127; + constexpr uint32 RETURN_KEYCODE = 13; + constexpr uint32 CONTROL_KEYCODE = 32; + + class swkbdCallbacks + { + public: + virtual void showSoftwareKeyboard(const std::string& initialText, sint32 maxLength) = 0; + virtual void hideSoftwareKeyboard() = 0; + }; + + void setSwkbdCallbacks(const std::shared_ptr& callbacks); + + void render(bool mainWindow); + bool hasKeyboardInputHook(); + void keyInput(uint32 keyCode); void load(); -} + +} // namespace swkbd diff --git a/src/android/app/src/main/cpp/AndroidSwkbdCallbacks.cpp b/src/android/app/src/main/cpp/AndroidSwkbdCallbacks.cpp new file mode 100644 index 00000000..af0fef7d --- /dev/null +++ b/src/android/app/src/main/cpp/AndroidSwkbdCallbacks.cpp @@ -0,0 +1,27 @@ +#include "AndroidSwkbdCallbacks.h" +#include "JNIUtils.h" + +AndroidSwkbdCallbacks::AndroidSwkbdCallbacks() +{ + JNIUtils::ScopedJNIENV env; + m_emulationActivityClass = JNIUtils::Scopedjclass("info/cemu/Cemu/emulation/EmulationActivity"); + m_showSoftwareKeyboardMethodID = env->GetStaticMethodID(*m_emulationActivityClass, "showEmulationTextInput", "(Ljava/lang/String;I)V"); + m_hideSoftwareKeyboardMethodID = env->GetStaticMethodID(*m_emulationActivityClass, "hideEmulationTextInput", "()V"); +} + +void AndroidSwkbdCallbacks::showSoftwareKeyboard(const std::string& initialText, sint32 maxLength) +{ + std::thread([&, this]() { + JNIUtils::ScopedJNIENV env; + jstring j_initialText = env->NewStringUTF(initialText.c_str()); + JNIUtils::ScopedJNIENV()->CallStaticVoidMethod(*m_emulationActivityClass, m_showSoftwareKeyboardMethodID, j_initialText, maxLength); + env->DeleteLocalRef(j_initialText); + }).join(); +} + +void AndroidSwkbdCallbacks::hideSoftwareKeyboard() +{ + std::thread([this]() { + JNIUtils::ScopedJNIENV()->CallStaticVoidMethod(*m_emulationActivityClass, m_hideSoftwareKeyboardMethodID); + }).join(); +} diff --git a/src/android/app/src/main/cpp/AndroidSwkbdCallbacks.h b/src/android/app/src/main/cpp/AndroidSwkbdCallbacks.h new file mode 100644 index 00000000..845223e3 --- /dev/null +++ b/src/android/app/src/main/cpp/AndroidSwkbdCallbacks.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Cafe/OS/libs/swkbd/swkbd.h" +#include "JNIUtils.h" +class AndroidSwkbdCallbacks : public swkbd::swkbdCallbacks +{ + JNIUtils::Scopedjclass m_emulationActivityClass; + jmethodID m_showSoftwareKeyboardMethodID; + jmethodID m_hideSoftwareKeyboardMethodID; + + public: + AndroidSwkbdCallbacks(); + void showSoftwareKeyboard(const std::string& initialText, sint32 maxLength) override; + void hideSoftwareKeyboard() override; +}; diff --git a/src/android/app/src/main/cpp/CMakeLists.txt b/src/android/app/src/main/cpp/CMakeLists.txt index 890fe7b0..65c96e9a 100644 --- a/src/android/app/src/main/cpp/CMakeLists.txt +++ b/src/android/app/src/main/cpp/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(CemuAndroid SHARED AndroidAudio.cpp AndroidEmulatedController.cpp + AndroidSwkbdCallbacks.cpp CMakeLists.txt GameTitleLoader.cpp Image.cpp @@ -11,6 +12,7 @@ add_library(CemuAndroid SHARED NativeInput.cpp NativeLib.cpp NativeSettings.cpp + NativeSwkbd.cpp ) target_link_libraries(CemuAndroid PRIVATE diff --git a/src/android/app/src/main/cpp/JNIUtils.cpp b/src/android/app/src/main/cpp/JNIUtils.cpp index 7b978f74..21e1fd0d 100644 --- a/src/android/app/src/main/cpp/JNIUtils.cpp +++ b/src/android/app/src/main/cpp/JNIUtils.cpp @@ -7,6 +7,8 @@ namespace JNIUtils std::string JNIUtils::JStringToString(JNIEnv* env, jstring jstr) { + if (jstr == nullptr) + return {}; const char* c_str = env->GetStringUTFChars(jstr, nullptr); std::string str(c_str); env->ReleaseStringUTFChars(jstr, c_str); diff --git a/src/android/app/src/main/cpp/NativeSwkbd.cpp b/src/android/app/src/main/cpp/NativeSwkbd.cpp new file mode 100644 index 00000000..d6f4658c --- /dev/null +++ b/src/android/app/src/main/cpp/NativeSwkbd.cpp @@ -0,0 +1,56 @@ +#include "JNIUtils.h" + +#include "Cafe/OS/libs/swkbd/swkbd.h" +#include "AndroidSwkbdCallbacks.h" + +namespace NativeSwkbd +{ + std::shared_ptr s_swkbdCallbacks; + std::string s_currentInputText; + struct StrDiffs + { + size_t newTextStartIndex; + size_t numberOfCharacterToDelete; + }; + StrDiffs getStringDiffs(const std::string& newText, const std::string& currentText) + { + if (newText.length() < currentText.length() && currentText.starts_with(newText)) + return {newText.length(), currentText.length() - newText.length()}; + if (newText.length() >= currentText.length() && newText.starts_with(currentText)) + return {currentText.length(), 0}; + return {0, currentText.length()}; + } +} // namespace NativeSwkbd + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +Java_info_cemu_Cemu_nativeinterface_NativeSwkbd_initializeSwkbd([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) +{ + if (NativeSwkbd::s_swkbdCallbacks != nullptr) + return; + NativeSwkbd::s_swkbdCallbacks = std::make_shared(); + swkbd::setSwkbdCallbacks(NativeSwkbd::s_swkbdCallbacks); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +Java_info_cemu_Cemu_nativeinterface_NativeSwkbd_setCurrentInputText([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring initial_text) +{ + NativeSwkbd::s_currentInputText = JNIUtils::JStringToString(env, initial_text); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +Java_info_cemu_Cemu_nativeinterface_NativeSwkbd_onTextChanged([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_text) +{ + std::string text = JNIUtils::JStringToString(env, j_text); + auto stringDiff = NativeSwkbd::getStringDiffs(text, NativeSwkbd::s_currentInputText); + for (size_t i = 0; i < stringDiff.numberOfCharacterToDelete; i++) + swkbd::keyInput(swkbd::BACKSPACE_KEYCODE); + for (size_t i = stringDiff.newTextStartIndex; i < text.length(); i++) + swkbd::keyInput(text.at(i)); + NativeSwkbd::s_currentInputText = std::move(text); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +Java_info_cemu_Cemu_nativeinterface_NativeSwkbd_onFinishedInputEdit([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) +{ + swkbd::keyInput(swkbd::RETURN_KEYCODE); +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/CemuApplication.java b/src/android/app/src/main/java/info/cemu/Cemu/CemuApplication.java index 45120eec..be3e2661 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/CemuApplication.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/CemuApplication.java @@ -7,6 +7,7 @@ import java.io.File; import info.cemu.Cemu.nativeinterface.NativeEmulation; import info.cemu.Cemu.nativeinterface.NativeGraphicPacks; +import info.cemu.Cemu.nativeinterface.NativeSwkbd; public class CemuApplication extends Application { static { @@ -37,6 +38,7 @@ public class CemuApplication extends Application { NativeEmulation.setDPI(displayMetrics.density); NativeEmulation.initializeActiveSettings(getInternalFolder().toString(), getInternalFolder().toString()); NativeEmulation.initializeEmulation(); + NativeSwkbd.initializeSwkbd(); NativeGraphicPacks.refreshGraphicPacks(); } } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationActivity.java b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationActivity.java index 2d5b6e68..1dc1c7e7 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationActivity.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationActivity.java @@ -1,12 +1,15 @@ package info.cemu.Cemu.emulation; +import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.text.InputFilter; import android.view.KeyEvent; import android.view.MotionEvent; import androidx.activity.OnBackPressedCallback; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; @@ -16,17 +19,59 @@ import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import java.util.Objects; + +import info.cemu.Cemu.BuildConfig; import info.cemu.Cemu.R; import info.cemu.Cemu.databinding.ActivityEmulationBinding; import info.cemu.Cemu.input.InputManager; +import info.cemu.Cemu.nativeinterface.NativeSwkbd; public class EmulationActivity extends AppCompatActivity implements Observer { - private EmulationViewModel viewModel; private boolean hasEmulationError; - public static final String LAUNCH_PATH = "LAUNCH_PATH"; - private ActivityEmulationBinding binding; - private EmulationFragment emulationFragment; + public static final String EXTRA_LAUNCH_PATH = BuildConfig.APPLICATION_ID + ".LaunchPath"; private final InputManager inputManager = new InputManager(); + private static EmulationActivity emulationActivityInstance; + private AlertDialog emulationTextInputDialog; + + /** + * This method is called by swkbd using JNI. + */ + @SuppressWarnings("unused") + public static void showEmulationTextInput(String initialText, int maxLength) { + if (emulationActivityInstance == null || emulationActivityInstance.emulationTextInputDialog != null) + return; + NativeSwkbd.setCurrentInputText(initialText); + emulationActivityInstance.runOnUiThread(() -> { + var inputEditTextLayout = emulationActivityInstance.getLayoutInflater().inflate(R.layout.layout_emulation_input, null); + EmulationTextInputEditText inputEditText = inputEditTextLayout.requireViewById(R.id.emulation_input_text); + inputEditText.updateText(initialText); + var dialog = new MaterialAlertDialogBuilder(emulationActivityInstance) + .setView(inputEditTextLayout) + .setCancelable(false) + .setPositiveButton(R.string.done, (d, w) -> { + }).show(); + var doneButton = Objects.requireNonNull(dialog.getButton(DialogInterface.BUTTON_POSITIVE)); + doneButton.setEnabled(false); + doneButton.setOnClickListener(v -> inputEditText.onFinishedEdit()); + inputEditText.setOnTextChangedListener(s -> doneButton.setEnabled(s.length() > 0)); + if (maxLength > 0) + inputEditText.appendFilter(new InputFilter.LengthFilter(maxLength)); + emulationActivityInstance.emulationTextInputDialog = dialog; + }); + } + + /** + * This method is called by swkbd using JNI. + */ + @SuppressWarnings("unused") + public static void hideEmulationTextInput() { + if (emulationActivityInstance == null || emulationActivityInstance.emulationTextInputDialog == null) + return; + var textInputDialog = emulationActivityInstance.emulationTextInputDialog; + emulationActivityInstance.emulationTextInputDialog = null; + emulationActivityInstance.runOnUiThread(textInputDialog::dismiss); + } @Override public boolean onGenericMotionEvent(MotionEvent event) { @@ -45,6 +90,7 @@ public class EmulationActivity extends AppCompatActivity implements Observer+=]+$"); + + public EmulationTextInputEditText(@NonNull Context context) { + this(context, null); + } + + public EmulationTextInputEditText(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, com.google.android.material.R.attr.editTextStyle); + } + + public void appendFilter(InputFilter inputFilter) { + setFilters(Stream.concat(Arrays.stream(getFilters()), Stream.of(inputFilter)).toArray(InputFilter[]::new)); + } + + public void updateText(String text) { + boolean hasFocus = hasFocus(); + if (hasFocus) + clearFocus(); + setText(text); + if (hasFocus) + requestFocus(); + } + + public EmulationTextInputEditText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + appendFilter((source, start, end, dest, dstart, dend) -> { + if (INPUT_PATTERN.matcher(source).matches()) + return null; + return ""; + }); + setInputType(EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_NORMAL); + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence text, int start, int before, int count) { + if (!hasFocus()) + return; + if (onTextChangedListener != null) + onTextChangedListener.onTextChanged(text); + NativeSwkbd.onTextChanged(text.toString()); + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + } + + public interface OnTextChangedListener { + void onTextChanged(CharSequence text); + } + + + private OnTextChangedListener onTextChangedListener; + + @Override + public void onEditorAction(int actionCode) { + var text = getText(); + if (actionCode == EditorInfo.IME_ACTION_DONE && text != null && text.length() > 0) { + onFinishedEdit(); + } + super.onEditorAction(actionCode); + } + + public void onFinishedEdit() { + NativeSwkbd.onFinishedInputEdit(); + } + + public void setOnTextChangedListener(OnTextChangedListener onTextChangedListener) { + this.onTextChangedListener = onTextChangedListener; + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GamesFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GamesFragment.java index 0e7010fa..75a11c39 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GamesFragment.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GamesFragment.java @@ -48,7 +48,7 @@ public class GamesFragment extends Fragment { currentGamePaths = new HashSet<>(NativeSettings.getGamesPaths()); gameAdapter = new GameAdapter(game -> { Intent intent = new Intent(getContext(), EmulationActivity.class); - intent.putExtra(EmulationActivity.LAUNCH_PATH, game.path()); + intent.putExtra(EmulationActivity.EXTRA_LAUNCH_PATH, game.path()); startActivity(intent); }); gameListViewModel = new ViewModelProvider(this).get(GameListViewModel.class); @@ -74,6 +74,8 @@ public class GamesFragment extends Fragment { MenuInflater inflater = requireActivity().getMenuInflater(); inflater.inflate(R.menu.game, menu); Game selectedGame = gameAdapter.getSelectedGame(); + if (selectedGame == null) + return; menu.findItem(R.id.favorite).setChecked(selectedGame.isFavorite()); menu.findItem(R.id.remove_shader_caches).setEnabled(NativeGameTitles.titleHasShaderCacheFiles(selectedGame.titleId())); } @@ -161,7 +163,6 @@ public class GamesFragment extends Fragment { }); recyclerView.setAdapter(gameAdapter); - return binding.getRoot(); } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/nativeinterface/NativeSwkbd.java b/src/android/app/src/main/java/info/cemu/Cemu/nativeinterface/NativeSwkbd.java new file mode 100644 index 00000000..7a2bae02 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/nativeinterface/NativeSwkbd.java @@ -0,0 +1,11 @@ +package info.cemu.Cemu.nativeinterface; + +public class NativeSwkbd { + public static native void initializeSwkbd(); + + public static native void setCurrentInputText(String text); + + public static native void onTextChanged(String text); + + public static native void onFinishedInputEdit(); +} diff --git a/src/android/app/src/main/res/layout/layout_emulation_input.xml b/src/android/app/src/main/res/layout/layout_emulation_input.xml new file mode 100644 index 00000000..23704a13 --- /dev/null +++ b/src/android/app/src/main/res/layout/layout_emulation_input.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 98ba4a0a..77d6b2db 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -238,4 +238,6 @@ Russian Taiwanese Console language + Input text + Done \ No newline at end of file diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 0939c114..d6aa4c31 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -1613,7 +1613,7 @@ void MainWindow::OnKeyUp(wxKeyEvent& event) { event.Skip(); - if (swkbd_hasKeyboardInputHook()) + if (swkbd::hasKeyboardInputHook()) return; const auto code = event.GetKeyCode(); @@ -1640,8 +1640,8 @@ void MainWindow::OnKeyDown(wxKeyEvent& event) void MainWindow::OnChar(wxKeyEvent& event) { - if (swkbd_hasKeyboardInputHook()) - swkbd_keyInput(event.GetUnicodeKey()); + if (swkbd::hasKeyboardInputHook()) + swkbd::keyInput(event.GetUnicodeKey()); // event.Skip(); } diff --git a/src/gui/PadViewFrame.cpp b/src/gui/PadViewFrame.cpp index f23cbb77..9e4fadb3 100644 --- a/src/gui/PadViewFrame.cpp +++ b/src/gui/PadViewFrame.cpp @@ -149,7 +149,7 @@ void PadViewFrame::OnKeyUp(wxKeyEvent& event) { event.Skip(); - if (swkbd_hasKeyboardInputHook()) + if (swkbd::hasKeyboardInputHook()) return; const auto code = event.GetKeyCode(); @@ -173,8 +173,8 @@ void PadViewFrame::OnGesturePan(wxPanGestureEvent& event) void PadViewFrame::OnChar(wxKeyEvent& event) { - if (swkbd_hasKeyboardInputHook()) - swkbd_keyInput(event.GetUnicodeKey()); + if (swkbd::hasKeyboardInputHook()) + swkbd::keyInput(event.GetUnicodeKey()); event.Skip(); }