android: Implement basic software keyboard applet.
parent
58ede89c60
commit
d5ebfc8e21
@ -0,0 +1,35 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "jni/android_common/android_common.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include "common/string_util.h"
|
||||||
|
|
||||||
|
std::string GetJString(JNIEnv* env, jstring jstr) {
|
||||||
|
if (!jstr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const jchar* jchars = env->GetStringChars(jstr, nullptr);
|
||||||
|
const jsize length = env->GetStringLength(jstr);
|
||||||
|
const std::u16string_view string_view(reinterpret_cast<const char16_t*>(jchars), length);
|
||||||
|
const std::string converted_string = Common::UTF16ToUTF8(string_view);
|
||||||
|
env->ReleaseStringChars(jstr, jchars);
|
||||||
|
|
||||||
|
return converted_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring ToJString(JNIEnv* env, std::string_view str) {
|
||||||
|
const std::u16string converted_string = Common::UTF8ToUTF16(str);
|
||||||
|
return env->NewString(reinterpret_cast<const jchar*>(converted_string.data()),
|
||||||
|
static_cast<jint>(converted_string.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring ToJString(JNIEnv* env, std::u16string_view str) {
|
||||||
|
return ToJString(env, Common::UTF16ToUTF8(str));
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
std::string GetJString(JNIEnv* env, jstring jstr);
|
||||||
|
jstring ToJString(JNIEnv* env, std::string_view str);
|
||||||
|
jstring ToJString(JNIEnv* env, std::u16string_view str);
|
@ -0,0 +1,277 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
|
#include "core/core.h"
|
||||||
|
#include "jni/android_common/android_common.h"
|
||||||
|
#include "jni/applets/software_keyboard.h"
|
||||||
|
#include "jni/id_cache.h"
|
||||||
|
|
||||||
|
static jclass s_software_keyboard_class;
|
||||||
|
static jclass s_keyboard_config_class;
|
||||||
|
static jclass s_keyboard_data_class;
|
||||||
|
static jmethodID s_swkbd_execute_normal;
|
||||||
|
static jmethodID s_swkbd_execute_inline;
|
||||||
|
|
||||||
|
namespace SoftwareKeyboard {
|
||||||
|
|
||||||
|
static jobject ToJKeyboardParams(const Core::Frontend::KeyboardInitializeParameters& config) {
|
||||||
|
JNIEnv* env = IDCache::GetEnvForThread();
|
||||||
|
jobject object = env->AllocObject(s_keyboard_config_class);
|
||||||
|
|
||||||
|
env->SetObjectField(object,
|
||||||
|
env->GetFieldID(s_keyboard_config_class, "ok_text", "Ljava/lang/String;"),
|
||||||
|
ToJString(env, config.ok_text));
|
||||||
|
env->SetObjectField(
|
||||||
|
object, env->GetFieldID(s_keyboard_config_class, "header_text", "Ljava/lang/String;"),
|
||||||
|
ToJString(env, config.header_text));
|
||||||
|
env->SetObjectField(object,
|
||||||
|
env->GetFieldID(s_keyboard_config_class, "sub_text", "Ljava/lang/String;"),
|
||||||
|
ToJString(env, config.sub_text));
|
||||||
|
env->SetObjectField(
|
||||||
|
object, env->GetFieldID(s_keyboard_config_class, "guide_text", "Ljava/lang/String;"),
|
||||||
|
ToJString(env, config.guide_text));
|
||||||
|
env->SetObjectField(
|
||||||
|
object, env->GetFieldID(s_keyboard_config_class, "initial_text", "Ljava/lang/String;"),
|
||||||
|
ToJString(env, config.initial_text));
|
||||||
|
env->SetShortField(object,
|
||||||
|
env->GetFieldID(s_keyboard_config_class, "left_optional_symbol_key", "S"),
|
||||||
|
static_cast<jshort>(config.left_optional_symbol_key));
|
||||||
|
env->SetShortField(object,
|
||||||
|
env->GetFieldID(s_keyboard_config_class, "right_optional_symbol_key", "S"),
|
||||||
|
static_cast<jshort>(config.right_optional_symbol_key));
|
||||||
|
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "max_text_length", "I"),
|
||||||
|
static_cast<jint>(config.max_text_length));
|
||||||
|
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "min_text_length", "I"),
|
||||||
|
static_cast<jint>(config.min_text_length));
|
||||||
|
env->SetIntField(object,
|
||||||
|
env->GetFieldID(s_keyboard_config_class, "initial_cursor_position", "I"),
|
||||||
|
static_cast<jint>(config.initial_cursor_position));
|
||||||
|
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "type", "I"),
|
||||||
|
static_cast<jint>(config.type));
|
||||||
|
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "password_mode", "I"),
|
||||||
|
static_cast<jint>(config.password_mode));
|
||||||
|
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "text_draw_type", "I"),
|
||||||
|
static_cast<jint>(config.text_draw_type));
|
||||||
|
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "key_disable_flags", "I"),
|
||||||
|
static_cast<jint>(config.key_disable_flags.raw));
|
||||||
|
env->SetBooleanField(object,
|
||||||
|
env->GetFieldID(s_keyboard_config_class, "use_blur_background", "Z"),
|
||||||
|
static_cast<jboolean>(config.use_blur_background));
|
||||||
|
env->SetBooleanField(object,
|
||||||
|
env->GetFieldID(s_keyboard_config_class, "enable_backspace_button", "Z"),
|
||||||
|
static_cast<jboolean>(config.enable_backspace_button));
|
||||||
|
env->SetBooleanField(object,
|
||||||
|
env->GetFieldID(s_keyboard_config_class, "enable_return_button", "Z"),
|
||||||
|
static_cast<jboolean>(config.enable_return_button));
|
||||||
|
env->SetBooleanField(object,
|
||||||
|
env->GetFieldID(s_keyboard_config_class, "disable_cancel_button", "Z"),
|
||||||
|
static_cast<jboolean>(config.disable_cancel_button));
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidKeyboard::ResultData AndroidKeyboard::ResultData::CreateFromFrontend(jobject object) {
|
||||||
|
JNIEnv* env = IDCache::GetEnvForThread();
|
||||||
|
const jstring string = reinterpret_cast<jstring>(env->GetObjectField(
|
||||||
|
object, env->GetFieldID(s_keyboard_data_class, "text", "Ljava/lang/String;")));
|
||||||
|
return ResultData{GetJString(env, string),
|
||||||
|
static_cast<Service::AM::Applets::SwkbdResult>(env->GetIntField(
|
||||||
|
object, env->GetFieldID(s_keyboard_data_class, "result", "I")))};
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidKeyboard::~AndroidKeyboard() = default;
|
||||||
|
|
||||||
|
void AndroidKeyboard::InitializeKeyboard(
|
||||||
|
bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters,
|
||||||
|
SubmitNormalCallback submit_normal_callback_, SubmitInlineCallback submit_inline_callback_) {
|
||||||
|
if (is_inline) {
|
||||||
|
LOG_WARNING(
|
||||||
|
Frontend,
|
||||||
|
"(STUBBED) called, backend requested to initialize the inline software keyboard.");
|
||||||
|
|
||||||
|
submit_inline_callback = std::move(submit_inline_callback_);
|
||||||
|
} else {
|
||||||
|
LOG_WARNING(
|
||||||
|
Frontend,
|
||||||
|
"(STUBBED) called, backend requested to initialize the normal software keyboard.");
|
||||||
|
|
||||||
|
submit_normal_callback = std::move(submit_normal_callback_);
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters = std::move(initialize_parameters);
|
||||||
|
|
||||||
|
LOG_INFO(Frontend,
|
||||||
|
"\nKeyboardInitializeParameters:"
|
||||||
|
"\nok_text={}"
|
||||||
|
"\nheader_text={}"
|
||||||
|
"\nsub_text={}"
|
||||||
|
"\nguide_text={}"
|
||||||
|
"\ninitial_text={}"
|
||||||
|
"\nmax_text_length={}"
|
||||||
|
"\nmin_text_length={}"
|
||||||
|
"\ninitial_cursor_position={}"
|
||||||
|
"\ntype={}"
|
||||||
|
"\npassword_mode={}"
|
||||||
|
"\ntext_draw_type={}"
|
||||||
|
"\nkey_disable_flags={}"
|
||||||
|
"\nuse_blur_background={}"
|
||||||
|
"\nenable_backspace_button={}"
|
||||||
|
"\nenable_return_button={}"
|
||||||
|
"\ndisable_cancel_button={}",
|
||||||
|
Common::UTF16ToUTF8(parameters.ok_text), Common::UTF16ToUTF8(parameters.header_text),
|
||||||
|
Common::UTF16ToUTF8(parameters.sub_text), Common::UTF16ToUTF8(parameters.guide_text),
|
||||||
|
Common::UTF16ToUTF8(parameters.initial_text), parameters.max_text_length,
|
||||||
|
parameters.min_text_length, parameters.initial_cursor_position, parameters.type,
|
||||||
|
parameters.password_mode, parameters.text_draw_type, parameters.key_disable_flags.raw,
|
||||||
|
parameters.use_blur_background, parameters.enable_backspace_button,
|
||||||
|
parameters.enable_return_button, parameters.disable_cancel_button);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidKeyboard::ShowNormalKeyboard() const {
|
||||||
|
LOG_DEBUG(Frontend, "called, backend requested to show the normal software keyboard.");
|
||||||
|
|
||||||
|
ResultData data{};
|
||||||
|
|
||||||
|
// Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber.
|
||||||
|
std::thread([&] {
|
||||||
|
data = ResultData::CreateFromFrontend(IDCache::GetEnvForThread()->CallStaticObjectMethod(
|
||||||
|
s_software_keyboard_class, s_swkbd_execute_normal, ToJKeyboardParams(parameters)));
|
||||||
|
}).join();
|
||||||
|
|
||||||
|
SubmitNormalText(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidKeyboard::ShowTextCheckDialog(
|
||||||
|
Service::AM::Applets::SwkbdTextCheckResult text_check_result,
|
||||||
|
std::u16string text_check_message) const {
|
||||||
|
LOG_WARNING(Frontend, "(STUBBED) called, backend requested to show the text check dialog.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidKeyboard::ShowInlineKeyboard(
|
||||||
|
Core::Frontend::InlineAppearParameters appear_parameters) const {
|
||||||
|
LOG_WARNING(Frontend,
|
||||||
|
"(STUBBED) called, backend requested to show the inline software keyboard.");
|
||||||
|
|
||||||
|
LOG_INFO(Frontend,
|
||||||
|
"\nInlineAppearParameters:"
|
||||||
|
"\nmax_text_length={}"
|
||||||
|
"\nmin_text_length={}"
|
||||||
|
"\nkey_top_scale_x={}"
|
||||||
|
"\nkey_top_scale_y={}"
|
||||||
|
"\nkey_top_translate_x={}"
|
||||||
|
"\nkey_top_translate_y={}"
|
||||||
|
"\ntype={}"
|
||||||
|
"\nkey_disable_flags={}"
|
||||||
|
"\nkey_top_as_floating={}"
|
||||||
|
"\nenable_backspace_button={}"
|
||||||
|
"\nenable_return_button={}"
|
||||||
|
"\ndisable_cancel_button={}",
|
||||||
|
appear_parameters.max_text_length, appear_parameters.min_text_length,
|
||||||
|
appear_parameters.key_top_scale_x, appear_parameters.key_top_scale_y,
|
||||||
|
appear_parameters.key_top_translate_x, appear_parameters.key_top_translate_y,
|
||||||
|
appear_parameters.type, appear_parameters.key_disable_flags.raw,
|
||||||
|
appear_parameters.key_top_as_floating, appear_parameters.enable_backspace_button,
|
||||||
|
appear_parameters.enable_return_button, appear_parameters.disable_cancel_button);
|
||||||
|
|
||||||
|
// Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber.
|
||||||
|
m_is_inline_active = true;
|
||||||
|
std::thread([&] {
|
||||||
|
IDCache::GetEnvForThread()->CallStaticVoidMethod(
|
||||||
|
s_software_keyboard_class, s_swkbd_execute_inline, ToJKeyboardParams(parameters));
|
||||||
|
}).join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidKeyboard::HideInlineKeyboard() const {
|
||||||
|
LOG_WARNING(Frontend,
|
||||||
|
"(STUBBED) called, backend requested to hide the inline software keyboard.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidKeyboard::InlineTextChanged(
|
||||||
|
Core::Frontend::InlineTextParameters text_parameters) const {
|
||||||
|
LOG_WARNING(Frontend,
|
||||||
|
"(STUBBED) called, backend requested to change the inline keyboard text.");
|
||||||
|
|
||||||
|
LOG_INFO(Frontend,
|
||||||
|
"\nInlineTextParameters:"
|
||||||
|
"\ninput_text={}"
|
||||||
|
"\ncursor_position={}",
|
||||||
|
Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position);
|
||||||
|
|
||||||
|
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString,
|
||||||
|
text_parameters.input_text, text_parameters.cursor_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidKeyboard::ExitKeyboard() const {
|
||||||
|
LOG_WARNING(Frontend, "(STUBBED) called, backend requested to exit the software keyboard.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidKeyboard::SubmitInlineKeyboardText(std::u16string submitted_text) {
|
||||||
|
if (!m_is_inline_active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_current_text += submitted_text;
|
||||||
|
|
||||||
|
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text,
|
||||||
|
m_current_text.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidKeyboard::SubmitInlineKeyboardInput(int key_code) {
|
||||||
|
static constexpr int KEYCODE_BACK = 4;
|
||||||
|
static constexpr int KEYCODE_ENTER = 66;
|
||||||
|
static constexpr int KEYCODE_DEL = 67;
|
||||||
|
|
||||||
|
if (!m_is_inline_active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key_code) {
|
||||||
|
case KEYCODE_BACK:
|
||||||
|
case KEYCODE_ENTER:
|
||||||
|
m_is_inline_active = false;
|
||||||
|
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::DecidedEnter, m_current_text,
|
||||||
|
static_cast<s32>(m_current_text.size()));
|
||||||
|
break;
|
||||||
|
case KEYCODE_DEL:
|
||||||
|
m_current_text.pop_back();
|
||||||
|
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text,
|
||||||
|
m_current_text.size());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidKeyboard::SubmitNormalText(const ResultData& data) const {
|
||||||
|
submit_normal_callback(data.result, Common::UTF8ToUTF16(data.text), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InitJNI(JNIEnv* env) {
|
||||||
|
s_software_keyboard_class = reinterpret_cast<jclass>(
|
||||||
|
env->NewGlobalRef(env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard")));
|
||||||
|
s_keyboard_config_class = reinterpret_cast<jclass>(env->NewGlobalRef(
|
||||||
|
env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig")));
|
||||||
|
s_keyboard_data_class = reinterpret_cast<jclass>(env->NewGlobalRef(
|
||||||
|
env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardData")));
|
||||||
|
|
||||||
|
s_swkbd_execute_normal = env->GetStaticMethodID(
|
||||||
|
s_software_keyboard_class, "ExecuteNormal",
|
||||||
|
"(Lorg/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig;)Lorg/yuzu/yuzu_emu/"
|
||||||
|
"applets/SoftwareKeyboard$KeyboardData;");
|
||||||
|
s_swkbd_execute_inline =
|
||||||
|
env->GetStaticMethodID(s_software_keyboard_class, "ExecuteInline",
|
||||||
|
"(Lorg/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig;)V");
|
||||||
|
}
|
||||||
|
|
||||||
|
void CleanupJNI(JNIEnv* env) {
|
||||||
|
env->DeleteGlobalRef(s_software_keyboard_class);
|
||||||
|
env->DeleteGlobalRef(s_keyboard_config_class);
|
||||||
|
env->DeleteGlobalRef(s_keyboard_data_class);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace SoftwareKeyboard
|
@ -0,0 +1,78 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include "core/frontend/applets/software_keyboard.h"
|
||||||
|
|
||||||
|
namespace SoftwareKeyboard {
|
||||||
|
|
||||||
|
class AndroidKeyboard final : public Core::Frontend::SoftwareKeyboardApplet {
|
||||||
|
public:
|
||||||
|
~AndroidKeyboard() override;
|
||||||
|
|
||||||
|
void Close() const override {
|
||||||
|
ExitKeyboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InitializeKeyboard(bool is_inline,
|
||||||
|
Core::Frontend::KeyboardInitializeParameters initialize_parameters,
|
||||||
|
SubmitNormalCallback submit_normal_callback_,
|
||||||
|
SubmitInlineCallback submit_inline_callback_) override;
|
||||||
|
|
||||||
|
void ShowNormalKeyboard() const override;
|
||||||
|
|
||||||
|
void ShowTextCheckDialog(Service::AM::Applets::SwkbdTextCheckResult text_check_result,
|
||||||
|
std::u16string text_check_message) const override;
|
||||||
|
|
||||||
|
void ShowInlineKeyboard(
|
||||||
|
Core::Frontend::InlineAppearParameters appear_parameters) const override;
|
||||||
|
|
||||||
|
void HideInlineKeyboard() const override;
|
||||||
|
|
||||||
|
void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) const override;
|
||||||
|
|
||||||
|
void ExitKeyboard() const override;
|
||||||
|
|
||||||
|
void SubmitInlineKeyboardText(std::u16string submitted_text);
|
||||||
|
|
||||||
|
void SubmitInlineKeyboardInput(int key_code);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct ResultData {
|
||||||
|
static ResultData CreateFromFrontend(jobject object);
|
||||||
|
|
||||||
|
std::string text;
|
||||||
|
Service::AM::Applets::SwkbdResult result{};
|
||||||
|
};
|
||||||
|
|
||||||
|
void SubmitNormalText(const ResultData& result) const;
|
||||||
|
|
||||||
|
Core::Frontend::KeyboardInitializeParameters parameters{};
|
||||||
|
|
||||||
|
mutable SubmitNormalCallback submit_normal_callback;
|
||||||
|
mutable SubmitInlineCallback submit_inline_callback;
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable bool m_is_inline_active{};
|
||||||
|
std::u16string m_current_text;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Should be called in JNI_Load
|
||||||
|
void InitJNI(JNIEnv* env);
|
||||||
|
|
||||||
|
// Should be called in JNI_Unload
|
||||||
|
void CleanupJNI(JNIEnv* env);
|
||||||
|
|
||||||
|
} // namespace SoftwareKeyboard
|
||||||
|
|
||||||
|
// Native function calls
|
||||||
|
extern "C" {
|
||||||
|
JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateFilters(
|
||||||
|
JNIEnv* env, jclass clazz, jstring text);
|
||||||
|
|
||||||
|
JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateInput(
|
||||||
|
JNIEnv* env, jclass clazz, jstring text);
|
||||||
|
}
|
Loading…
Reference in New Issue