Implement Software Keyboard applet

This implements the non-inline version of the Software Keyboard (swkbd) applet, which games use to get text input from the user.
This commit is contained in:
MCredstoner2004 2022-06-28 02:25:01 -05:00
parent a9ee06914d
commit f9a0394577
No known key found for this signature in database
GPG Key ID: A412203FCA8E8F64
16 changed files with 1149 additions and 9 deletions

View File

@ -263,6 +263,8 @@ add_library(skyline SHARED
${source_DIR}/skyline/applet/applet_creator.cpp
${source_DIR}/skyline/applet/controller_applet.cpp
${source_DIR}/skyline/applet/player_select_applet.cpp
${source_DIR}/skyline/applet/swkbd/software_keyboard_applet.cpp
${source_DIR}/skyline/applet/swkbd/software_keyboard_config.cpp
${source_DIR}/skyline/services/codec/IHardwareOpusDecoder.cpp
${source_DIR}/skyline/services/codec/IHardwareOpusDecoderManager.cpp
${source_DIR}/skyline/services/hid/IHidServer.cpp

View File

@ -65,7 +65,7 @@
<activity
android:name="emu.skyline.EmulationActivity"
android:configChanges="orientation|screenSize"
android:configChanges="orientation|screenSize|uiMode"
android:exported="true"
android:launchMode="singleTask">
<meta-data

View File

@ -4,6 +4,7 @@
#include "controller_applet.h"
#include "player_select_applet.h"
#include "applet_creator.h"
#include "swkbd/software_keyboard_applet.h"
namespace skyline::applet {
std::shared_ptr<service::am::IApplet> CreateApplet(
@ -17,6 +18,8 @@ namespace skyline::applet {
return std::make_shared<ControllerApplet>(state, manager, std::move(onAppletStateChanged), std::move(onNormalDataPushFromApplet), std::move(onInteractiveDataPushFromApplet), appletMode);
case AppletId::LibraryAppletPlayerSelect:
return std::make_shared<PlayerSelectApplet>(state, manager, std::move(onAppletStateChanged), std::move(onNormalDataPushFromApplet), std::move(onInteractiveDataPushFromApplet), appletMode);
case AppletId::LibraryAppletSwkbd:
return std::make_shared<swkbd::SoftwareKeyboardApplet>(state, manager, std::move(onAppletStateChanged), std::move(onNormalDataPushFromApplet), std::move(onInteractiveDataPushFromApplet), appletMode);
default:
throw exception("Unimplemented Applet: 0x{:X} ({})", static_cast<u32>(appletId), ToString(appletId));
}

View File

@ -0,0 +1,179 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
// Copyright © 2019-2022 Ryujinx Team and Contributors
#include <codecvt>
#include <services/am/storage/ObjIStorage.h>
#include <common/settings.h>
#include "software_keyboard_applet.h"
#include <jvm.h>
class Utf8Utf16Converter : public std::codecvt<char16_t, char8_t, std::mbstate_t> {
public:
~Utf8Utf16Converter() override = default;
};
namespace skyline::applet::swkbd {
static void WriteStringToSpan(span<u8> chars, std::u16string_view text, bool useUtf8Storage) {
if (useUtf8Storage) {
auto u8chars{chars.cast<char8_t>()};
Utf8Utf16Converter::state_type convert_state;
const char16_t *from_next;
char8_t *to_next;
Utf8Utf16Converter().out(convert_state, text.data(), text.end(), from_next, u8chars.data(), u8chars.end().base(), to_next);
// Null terminate the string, if it isn't out of bounds
if (to_next < reinterpret_cast<const char8_t *>(text.end()))
*to_next = u8'\0';
} else {
std::memcpy(chars.data(), text.data(), std::min(text.size() * sizeof(char16_t), chars.size()));
// Null terminate the string, if it isn't out of bounds
if (text.size() * sizeof(char16_t) < chars.size())
*(reinterpret_cast<char16_t *>(chars.data()) + text.size()) = u'\0';
}
}
SoftwareKeyboardApplet::ValidationRequest::ValidationRequest(std::u16string_view text, bool useUtf8Storage) : size{sizeof(ValidationRequest)} {
WriteStringToSpan(chars, text, useUtf8Storage);
}
SoftwareKeyboardApplet::OutputResult::OutputResult(CloseResult closeResult, std::u16string_view text, bool useUtf8Storage) : closeResult{closeResult} {
WriteStringToSpan(chars, text, useUtf8Storage);
}
static std::u16string FillDefaultText(u32 minLength, u32 maxLength) {
std::u16string text{u"Skyline"};
while (text.size() < minLength)
text += u"Emulator" + text;
if (text.size() > maxLength)
text.resize(maxLength);
return text;
}
void SoftwareKeyboardApplet::SendResult() {
if (dialog)
state.jvm->CloseKeyboard(dialog);
PushNormalDataAndSignal(std::make_shared<service::am::ObjIStorage<OutputResult>>(state, manager, OutputResult{currentResult, currentText, config.commonConfig.isUseUtf8}));
onAppletStateChanged->Signal();
}
SoftwareKeyboardApplet::SoftwareKeyboardApplet(
const DeviceState &state,
service::ServiceManager &manager,
std::shared_ptr<kernel::type::KEvent> onAppletStateChanged,
std::shared_ptr<kernel::type::KEvent> onNormalDataPushFromApplet,
std::shared_ptr<kernel::type::KEvent> onInteractiveDataPushFromApplet,
service::applet::LibraryAppletMode appletMode)
: IApplet{state,
manager,
std::move(onAppletStateChanged),
std::move(onNormalDataPushFromApplet),
std::move(onInteractiveDataPushFromApplet),
appletMode} {
if (appletMode != service::applet::LibraryAppletMode::AllForeground)
throw exception("Inline Software Keyboard not implemeted");
}
Result SoftwareKeyboardApplet::Start() {
std::scoped_lock lock{inputDataMutex};
auto commonArgs{normalInputData.front()->GetSpan().as<service::applet::CommonArguments>()};
normalInputData.pop();
auto configSpan{normalInputData.front()->GetSpan()};
normalInputData.pop();
config = [&] {
if (commonArgs.apiVersion < 0x30007)
return KeyboardConfigVB{configSpan.as<KeyboardConfigV0>()};
else if (commonArgs.apiVersion < 0x6000B)
return KeyboardConfigVB{configSpan.as<KeyboardConfigV7>()};
else
return configSpan.as<KeyboardConfigVB>();
}();
Logger::Debug("Swkbd Config:\n* KeyboardMode: {}\n* InvalidCharFlags: {:#09b}\n* TextMaxLength: {}\n* TextMinLength: {}\n* PasswordMode: {}\n* InputFormMode: {}\n* IsUseNewLine: {}\n* IsUseTextCheck: {}",
static_cast<u32>(config.commonConfig.keyboardMode),
config.commonConfig.invalidCharFlags.raw,
config.commonConfig.textMaxLength,
config.commonConfig.textMinLength,
static_cast<u32>(config.commonConfig.passwordMode),
static_cast<u32>(config.commonConfig.inputFormMode),
config.commonConfig.isUseNewLine,
config.commonConfig.isUseTextCheck
);
auto maxChars{static_cast<u32>(SwkbdTextBytes / (config.commonConfig.isUseUtf8 ? sizeof(char8_t) : sizeof(char16_t)))};
config.commonConfig.textMaxLength = std::min(config.commonConfig.textMaxLength, maxChars);
if (config.commonConfig.textMaxLength == 0)
config.commonConfig.textMaxLength = maxChars;
config.commonConfig.textMinLength = std::min(config.commonConfig.textMinLength, config.commonConfig.textMaxLength);
if (config.commonConfig.textMaxLength > MaxOneLineChars)
config.commonConfig.inputFormMode = InputFormMode::MultiLine;
if (!normalInputData.empty() && config.commonConfig.initialStringLength > 0)
currentText = std::u16string(normalInputData.front()->GetSpan().subspan(config.commonConfig.initialStringOffset).cast<char16_t>().data(), config.commonConfig.initialStringLength);
dialog = state.jvm->ShowKeyboard(*reinterpret_cast<JvmManager::KeyboardConfig *>(&config), currentText);
if (!dialog) {
Logger::Warn("Couldn't show keyboard dialog, using default text");
currentResult = CloseResult::Enter;
currentText = FillDefaultText(config.commonConfig.textMinLength, config.commonConfig.textMaxLength);
} else {
auto result{state.jvm->WaitForSubmitOrCancel(dialog)};
currentResult = static_cast<CloseResult>(result.first);
currentText = result.second;
}
if (config.commonConfig.isUseTextCheck && currentResult == CloseResult::Enter) {
PushInteractiveDataAndSignal(std::make_shared<service::am::ObjIStorage<ValidationRequest>>(state, manager, ValidationRequest{currentText, config.commonConfig.isUseUtf8}));
validationPending = true;
} else {
SendResult();
}
return {};
}
Result SoftwareKeyboardApplet::GetResult() {
return {};
}
void SoftwareKeyboardApplet::PushNormalDataToApplet(std::shared_ptr<service::am::IStorage> data) {
std::scoped_lock lock{inputDataMutex};
normalInputData.emplace(data);
}
void SoftwareKeyboardApplet::PushInteractiveDataToApplet(std::shared_ptr<service::am::IStorage> data) {
if (validationPending) {
auto dataSpan{data->GetSpan()};
auto validationResult{dataSpan.as<ValidationResult>()};
if (validationResult.result == TextCheckResult::Success) {
validationPending = false;
SendResult();
} else {
if (dialog) {
if (static_cast<CloseResult>(state.jvm->ShowValidationResult(dialog, static_cast<JvmManager::KeyboardTextCheckResult>(validationResult.result), std::u16string(validationResult.chars.data()))) == CloseResult::Enter) {
// Accepted on confirmation dialog
validationPending = false;
SendResult();
} else {
// Cancelled or failed validation, go back to waiting for text
auto result{state.jvm->WaitForSubmitOrCancel(dialog)};
currentResult = static_cast<CloseResult>(result.first);
currentText = result.second;
if (currentResult == CloseResult::Enter) {
PushInteractiveDataAndSignal(std::make_shared<service::am::ObjIStorage<ValidationRequest>>(state, manager, ValidationRequest{currentText, config.commonConfig.isUseUtf8}));
} else {
SendResult();
}
}
} else {
std::array<u8, SwkbdTextBytes> chars{};
WriteStringToSpan(chars, std::u16string(validationResult.chars.data()), true);
std::string message{reinterpret_cast<char *>(chars.data())};
if (validationResult.result == TextCheckResult::ShowFailureDialog)
Logger::Warn("Sending default text despite being rejected by the guest with message: \"{}\"", message);
else
Logger::Debug("Guest asked to confirm default text with message: \"{}\"", message);
PushNormalDataAndSignal(std::make_shared<service::am::ObjIStorage<OutputResult>>(state, manager, OutputResult{CloseResult::Enter, currentText, config.commonConfig.isUseUtf8}));
}
}
}
}
}

View File

@ -0,0 +1,98 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
#pragma once
#include <services/am/applet/IApplet.h>
#include <services/applet/common_arguments.h>
#include <jvm.h>
#include "software_keyboard_config.h"
namespace skyline::applet::swkbd {
static_assert(sizeof(KeyboardConfigVB) == sizeof(JvmManager::KeyboardConfig));
/**
* @url https://switchbrew.org/wiki/Software_Keyboard
* @brief An implementation for the Software Keyboard (swkbd) Applet which handles translating guest applet transactions to the appropriate host behavior
*/
class SoftwareKeyboardApplet : public service::am::IApplet {
private:
/**
* @url https://switchbrew.org/wiki/Software_Keyboard#CloseResult
*/
enum class CloseResult : u32 {
Enter = 0x0,
Cancel = 0x1,
};
/**
* @url https://switchbrew.org/wiki/Software_Keyboard#TextCheckResult
*/
enum class TextCheckResult : u32 {
Success = 0x0,
ShowFailureDialog = 0x1,
ShowConfirmDialog = 0x2,
};
static constexpr u32 SwkbdTextBytes{0x7D4}; //!< Size of the returned IStorage buffer that's used to return the input text
static constexpr u32 MaxOneLineChars{32}; //!< The maximum number of characters for which anything other than InputFormMode::MultiLine is used
#pragma pack(push, 1)
/**
* @brief The final result after the swkbd has closed
*/
struct OutputResult {
CloseResult closeResult;
std::array<u8, SwkbdTextBytes> chars{};
OutputResult(CloseResult closeResult, std::u16string_view text, bool useUtf8Storage);
};
static_assert(sizeof(OutputResult) == 0x7D8);
/**
* @brief A request for validating a string inside guest code, this is pushed via the interactive queue
*/
struct ValidationRequest {
u64 size;
std::array<u8, SwkbdTextBytes> chars{};
ValidationRequest(std::u16string_view text, bool useUtf8Storage);
};
static_assert(sizeof(ValidationRequest) == 0x7DC);
/**
* @brief The result of validating text submitted to the guest
*/
struct ValidationResult {
TextCheckResult result;
std::array<char16_t, SwkbdTextBytes / sizeof(char16_t)> chars;
};
static_assert(sizeof(ValidationResult) == 0x7D8);
#pragma pack(pop)
std::mutex inputDataMutex;
std::queue<std::shared_ptr<service::am::IStorage>> normalInputData;
KeyboardConfigVB config{};
bool validationPending{};
std::u16string currentText{};
CloseResult currentResult{};
jobject dialog{};
void SendResult();
public:
SoftwareKeyboardApplet(const DeviceState &state, service::ServiceManager &manager, std::shared_ptr<kernel::type::KEvent> onAppletStateChanged, std::shared_ptr<kernel::type::KEvent> onNormalDataPushFromApplet, std::shared_ptr<kernel::type::KEvent> onInteractiveDataPushFromApplet, service::applet::LibraryAppletMode appletMode);
Result Start() override;
Result GetResult() override;
void PushNormalDataToApplet(std::shared_ptr<service::am::IStorage> data) override;
void PushInteractiveDataToApplet(std::shared_ptr<service::am::IStorage> data) override;
};
}

View File

@ -0,0 +1,13 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
// Copyright © 2019-2022 Ryujinx Team and Contributors
#include "software_keyboard_config.h"
namespace skyline::applet::swkbd {
KeyboardConfigVB::KeyboardConfigVB() = default;
KeyboardConfigVB::KeyboardConfigVB(const KeyboardConfigV7 &v7config) : commonConfig(v7config.commonConfig), separateTextPos(v7config.separateTextPos) {}
KeyboardConfigVB::KeyboardConfigVB(const KeyboardConfigV0 &v0config) : commonConfig(v0config.commonConfig) {}
}

View File

@ -0,0 +1,187 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
// Copyright © 2019-2022 Ryujinx Team and Contributors
#pragma once
#include <services/account/IAccountServiceForApplication.h>
namespace skyline::applet::swkbd {
/**
* @brief Specifies the characters the keyboard should allow you to input
* @url https://switchbrew.org/wiki/Software_Keyboard#KeyboardMode
*/
enum class KeyboardMode : u32 {
Full = 0x0,
Numeric = 0x1,
ASCII = 0x2,
FullLatin = 0x3,
Alphabet = 0x4,
SimplifiedChinese = 0x5,
TraditionalChinese = 0x6,
Korean = 0x7,
LanguageSet2 = 0x8,
LanguageSet2Latin = 0x9,
};
/**
* @brief Specifies the characters that you shouldn't be allowed to input
* @url https://switchbrew.org/wiki/Software_Keyboard#InvalidCharFlags
*/
union InvalidCharFlags {
u32 raw;
struct {
u32 _pad_ : 1;
u32 space : 1;
u32 atMark : 1;
u32 percent : 1;
u32 slash : 1;
u32 backslash : 1;
u32 numeric : 1;
u32 outsideOfDownloadCode : 1;
u32 outsideOfMiiNickName : 1;
} flags;
};
/**
* @brief Specifies where the cursor should initially be on the initial string
* @url https://switchbrew.org/wiki/Software_Keyboard#InitialCursorPos
*/
enum class InitialCursorPos : u32 {
First = 0x0,
Last = 0x1,
};
/**
* @url https://switchbrew.org/wiki/Software_Keyboard#PasswordMode
*/
enum class PasswordMode : u32 {
Show = 0x0,
Hide = 0x1, //!< Hides any inputted text to prevent a password from being leaked
};
/**
* @url https://switchbrew.org/wiki/Software_Keyboard#InputFormMode
* @note Only applies when 1 <= textMaxLength <= 32, otherwise Multiline is used
*/
enum class InputFormMode : u32 {
OneLine = 0x0,
MultiLine = 0x1,
Separate = 0x2, //!< Used with separateTextPos
};
/**
* @brief Specifies the language of custom dictionary entries
* @url https://switchbrew.org/wiki/Software_Keyboard#DictionaryLanguage
*/
enum class DictionaryLanguage : u16 {
Japanese = 0x00,
AmericanEnglish = 0x01,
CanadianFrench = 0x02,
LatinAmericanSpanish = 0x03,
Reserved1 = 0x04,
BritishEnglish = 0x05,
French = 0x06,
German = 0x07,
Spanish = 0x08,
Italian = 0x09,
Dutch = 0x0A,
Portuguese = 0x0B,
Russian = 0x0C,
Reserved2 = 0x0D,
SimplifiedChinesePinyin = 0x0E,
TraditionalChineseCangjie = 0x0F,
TraditionalChineseSimplifiedCangjie = 0x10,
TraditionalChineseZhuyin = 0x11,
Korean = 0x12,
};
/**
* @brief A descriptor of a custom dictionary entry
* @url https://switchbrew.org/wiki/Software_Keyboard#DictionaryInfo
*/
struct DictionaryInfo {
u32 offset;
u16 size;
DictionaryLanguage language;
};
/**
* @brief The keyboard config that's common across all versions
* @url https://switchbrew.org/wiki/Software_Keyboard#KeyboardConfig
*/
struct CommonKeyboardConfig {
KeyboardMode keyboardMode;
std::array<char16_t, 0x9> okText;
char16_t leftOptionalSymbolKey;
char16_t rightOptionalSymbolKey;
bool isPredictionEnabled;
u8 _pad0_;
InvalidCharFlags invalidCharFlags;
InitialCursorPos initialCursorPos;
std::array<char16_t, 0x41> headerText;
std::array<char16_t, 0x81> subText;
std::array<char16_t, 0x101> guideText;
u8 _pad2_[0x2];
u32 textMaxLength;
u32 textMinLength;
PasswordMode passwordMode;
InputFormMode inputFormMode;
bool isUseNewLine;
bool isUseUtf8;
bool isUseBlurBackground;
u8 _pad3_;
u32 initialStringOffset;
u32 initialStringLength;
u32 userDictionaryOffset;
u32 userDictionaryNum;
bool isUseTextCheck;
u8 reserved[0x3];
};
static_assert(sizeof(CommonKeyboardConfig) == 0x3D4);
/**
* @brief The keyboard config for the first api version
* @url https://switchbrew.org/wiki/Software_Keyboard#KeyboardConfig
*/
struct KeyboardConfigV0 {
CommonKeyboardConfig commonConfig;
u8 _pad0_[0x4];
u64 textCheckCallback{};
};
static_assert(sizeof(KeyboardConfigV0) == 0x3E0);
/**
* @brief The keyboard config as of api version 0x30007
* @url https://switchbrew.org/wiki/Software_Keyboard#KeyboardConfig
*/
struct KeyboardConfigV7 {
CommonKeyboardConfig commonConfig;
u8 _pad0_[0x4];
u64 textCheckCallback;
std::array<u32, 0x8> separateTextPos;
};
static_assert(sizeof(KeyboardConfigV7) == 0x400);
/**
* @brief The keyboard config as of api version 0x6000B
* @url https://switchbrew.org/wiki/Software_Keyboard#KeyboardConfig
*/
struct KeyboardConfigVB {
CommonKeyboardConfig commonConfig{};
std::array<u32, 0x8> separateTextPos{0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF};
std::array<DictionaryInfo, 0x18> customisedDictionaryInfoList{};
u8 customisedDictionaryCount{};
bool isCancelButtonDisabled{};
u8 reserved0[0xD];
u8 trigger{};
u8 reserved1[0x4];
KeyboardConfigVB();
KeyboardConfigVB(const KeyboardConfigV7 &v7config);
KeyboardConfigVB(const KeyboardConfigV0 &v0config);
};
static_assert(sizeof(KeyboardConfigVB) == 0x4C8);
}

View File

@ -54,12 +54,17 @@ namespace skyline {
thread_local inline JniEnvironment env;
JvmManager::JvmManager(JNIEnv *environ, jobject instance)
: instance(environ->NewGlobalRef(instance)),
instanceClass(reinterpret_cast<jclass>(environ->NewGlobalRef(environ->GetObjectClass(instance)))),
initializeControllersId(environ->GetMethodID(instanceClass, "initializeControllers", "()V")),
vibrateDeviceId(environ->GetMethodID(instanceClass, "vibrateDevice", "(I[J[I)V")),
clearVibrationDeviceId(environ->GetMethodID(instanceClass, "clearVibrationDevice", "(I)V")),
getVersionCodeId(environ->GetMethodID(instanceClass, "getVersionCode", "()I")) {
: instance{environ->NewGlobalRef(instance)},
instanceClass{reinterpret_cast<jclass>(environ->NewGlobalRef(environ->GetObjectClass(instance)))},
initializeControllersId{environ->GetMethodID(instanceClass, "initializeControllers", "()V")},
vibrateDeviceId{environ->GetMethodID(instanceClass, "vibrateDevice", "(I[J[I)V")},
clearVibrationDeviceId{environ->GetMethodID(instanceClass, "clearVibrationDevice", "(I)V")},
showKeyboardId{environ->GetMethodID(instanceClass, "showKeyboard", "(Ljava/nio/ByteBuffer;Ljava/lang/String;)Lemu/skyline/applet/swkbd/SoftwareKeyboardDialog;")},
waitForSubmitOrCancelId{environ->GetMethodID(instanceClass, "waitForSubmitOrCancel", "(Lemu/skyline/applet/swkbd/SoftwareKeyboardDialog;)[Ljava/lang/Object;")},
closeKeyboardId{environ->GetMethodID(instanceClass, "closeKeyboard", "(Lemu/skyline/applet/swkbd/SoftwareKeyboardDialog;)V")},
showValidationResultId{environ->GetMethodID(instanceClass, "showValidationResult", "(Lemu/skyline/applet/swkbd/SoftwareKeyboardDialog;ILjava/lang/String;)I")},
getVersionCodeId{environ->GetMethodID(instanceClass, "getVersionCode", "()I")},
getIntegerValueId{environ->GetMethodID(environ->FindClass("java/lang/Integer"), "intValue", "()I")} {
env.Initialize(environ);
}
@ -104,7 +109,42 @@ namespace skyline {
env->CallVoidMethod(instance, clearVibrationDeviceId, index);
}
jobject JvmManager::ShowKeyboard(KeyboardConfig &config, std::u16string initialText) {
auto buffer{env->NewDirectByteBuffer(&config, sizeof(KeyboardConfig))};
auto str{env->NewString(reinterpret_cast<const jchar *>(initialText.data()), static_cast<int>(initialText.length()))};
jobject localKeyboardDialog{env->CallObjectMethod(instance, showKeyboardId, buffer, str)};
env->DeleteLocalRef(buffer);
env->DeleteLocalRef(str);
auto keyboardDialog{env->NewGlobalRef(localKeyboardDialog)};
env->DeleteLocalRef(localKeyboardDialog);
return keyboardDialog;
}
std::pair<JvmManager::KeyboardCloseResult, std::u16string> JvmManager::WaitForSubmitOrCancel(jobject keyboardDialog) {
auto returnArray{reinterpret_cast<jobjectArray>(env->CallObjectMethod(instance, waitForSubmitOrCancelId, keyboardDialog))};
auto buttonInteger{env->GetObjectArrayElement(returnArray, 0)};
auto inputJString{reinterpret_cast<jstring>(env->GetObjectArrayElement(returnArray, 1))};
auto stringChars{env->GetStringChars(inputJString, nullptr)};
std::u16string input{stringChars, stringChars + env->GetStringLength(inputJString)};
env->ReleaseStringChars(inputJString, stringChars);
return {static_cast<KeyboardCloseResult>(env->CallIntMethod(buttonInteger, getIntegerValueId)), input};
}
void JvmManager::CloseKeyboard(jobject dialog) {
env->CallVoidMethod(instance, closeKeyboardId, dialog);
env->DeleteGlobalRef(dialog);
}
i32 JvmManager::GetVersionCode() {
return env->CallIntMethod(instance, getVersionCodeId);
}
JvmManager::KeyboardCloseResult JvmManager::ShowValidationResult(jobject dialog, KeyboardTextCheckResult checkResult, std::u16string message) {
auto str{env->NewString(reinterpret_cast<const jchar *>(message.data()), static_cast<int>(message.length()))};
auto result{static_cast<KeyboardCloseResult>(env->CallIntMethod(instance, showValidationResultId, dialog, checkResult, str))};
env->DeleteLocalRef(str);
return result;
}
}

View File

@ -23,6 +23,12 @@ namespace skyline {
*/
class JvmManager {
public:
using KeyboardHandle = jobject;
using KeyboardConfig = std::array<u8, 0x4C8>;
using KeyboardCloseResult = u32;
using KeyboardTextCheckResult = u32;
jobject instance; //!< A reference to the activity
jclass instanceClass; //!< The class of the activity
@ -106,6 +112,26 @@ namespace skyline {
*/
void ClearVibrationDevice(jint index);
/**
* @brief A call to EmulationActivity.showKeyboard in Kotlin
*/
KeyboardHandle ShowKeyboard(KeyboardConfig &config, std::u16string initialText);
/**
* @brief A call to EmulationActivity.waitForSubmitOrCancel in Kotlin
*/
std::pair<KeyboardCloseResult, std::u16string> WaitForSubmitOrCancel(KeyboardHandle dialog);
/**
* @brief A call to EmulationActivity.closeKeyboard in Kotlin
*/
void CloseKeyboard(KeyboardHandle dialog);
/**
* @brief A call to EmulationActivity.showValidationResult in Kotlin
*/
KeyboardCloseResult ShowValidationResult(KeyboardHandle dialog, KeyboardTextCheckResult checkResult, std::u16string message);
/**
* @brief A call to EmulationActivity.getVersionCode in Kotlin
* @return A version code in Vulkan's format with 14-bit patch + 10-bit major and minor components
@ -116,6 +142,12 @@ namespace skyline {
jmethodID initializeControllersId;
jmethodID vibrateDeviceId;
jmethodID clearVibrationDeviceId;
jmethodID showKeyboardId;
jmethodID waitForSubmitOrCancelId;
jmethodID closeKeyboardId;
jmethodID showValidationResultId;
jmethodID getVersionCodeId;
jmethodID getIntegerValueId;
};
}

View File

@ -43,7 +43,7 @@ class AppDialog : BottomSheetDialogFragment() {
private lateinit var binding : AppDialogBinding
private val item by lazy { requireArguments().getSerializable("item") as AppItem }
private val item by lazy { requireArguments().getSerializable("item")!! as AppItem }
/**
* This inflates the layout of the dialog after initial view creation

View File

@ -20,12 +20,20 @@ import androidx.core.content.getSystemService
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.updateMargins
import androidx.fragment.app.FragmentTransaction
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import emu.skyline.applet.swkbd.SoftwareKeyboardConfig
import emu.skyline.applet.swkbd.SoftwareKeyboardDialog
import emu.skyline.databinding.EmuActivityBinding
import emu.skyline.input.*
import emu.skyline.loader.getRomFormat
import emu.skyline.utils.ByteBufferSerializable
import emu.skyline.utils.Settings
import java.io.File
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.concurrent.FutureTask
import javax.inject.Inject
import kotlin.math.abs
@ -365,7 +373,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
// Stop forcing 60Hz on exit to allow the skyline UI to run at high refresh rates
getSystemService<DisplayManager>()?.unregisterDisplayListener(this)
force60HzRefreshRate(false);
force60HzRefreshRate(false)
}
override fun surfaceCreated(holder : SurfaceHolder) {
@ -574,6 +582,50 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
vibrators[index]?.cancel()
}
@Suppress("unused")
fun showKeyboard(buffer : ByteBuffer, initialText : String) : SoftwareKeyboardDialog? {
buffer.order(ByteOrder.LITTLE_ENDIAN)
val config = ByteBufferSerializable.createFromByteBuffer(SoftwareKeyboardConfig::class, buffer) as SoftwareKeyboardConfig
val keyboardDialog = SoftwareKeyboardDialog.newInstance(config, initialText)
runOnUiThread {
val transaction = supportFragmentManager.beginTransaction()
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
transaction
.add(android.R.id.content, keyboardDialog)
.addToBackStack(null)
.commit()
}
return keyboardDialog
}
@Suppress("unused")
fun waitForSubmitOrCancel(dialog : SoftwareKeyboardDialog) : Array<Any?> {
return dialog.waitForSubmitOrCancel().let { arrayOf(if (it.cancelled) 1 else 0, it.text) }
}
@Suppress("unused")
fun closeKeyboard(dialog : SoftwareKeyboardDialog) {
runOnUiThread { dialog.dismiss() }
}
@Suppress("unused")
fun showValidationResult(dialog : SoftwareKeyboardDialog, validationResult : Int, message : String) : Int {
val confirm = validationResult == SoftwareKeyboardDialog.validationConfirm
var accepted = false
val validatorResult = FutureTask { return@FutureTask accepted }
runOnUiThread {
val builder = MaterialAlertDialogBuilder(dialog.requireContext())
builder.setMessage(message)
builder.setPositiveButton(if (confirm) getString(android.R.string.ok) else getString(android.R.string.cancel)) { _, _ -> accepted = confirm }
if (confirm)
builder.setNegativeButton(getString(android.R.string.cancel)) { _, _ -> }
builder.setOnDismissListener { validatorResult.run() }
builder.show()
}
return if (validatorResult.get()) 0 else 1
}
/**
* @return A version code in Vulkan's format with 14-bit patch + 10-bit major and minor components
*/

View File

@ -0,0 +1,231 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
@file:OptIn(ExperimentalUnsignedTypes::class)
package emu.skyline.applet.swkbd
import emu.skyline.utils.*
fun getCodepointArray(vararg chars : Char) : IntArray {
val codepointArray = IntArray(chars.size)
for (i in chars.indices)
codepointArray[i] = chars[i].code
return codepointArray
}
val DownloadCodeCodepoints = getCodepointArray('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X')
val OutsideOfMiiNicknameCodepoints = getCodepointArray('@', '%', '\\', 'ō', 'Ō', '₽', '₩', '♥', '♡')
/**
* This data class matches KeyboardMode in skyline/applet/swkbd/software_keyboard_config.h
*/
data class KeyboardMode(
var mode : u32 = 0u
) : ByteBufferSerializable {
companion object {
val Full = KeyboardMode(0u)
val Numeric = KeyboardMode(1u)
val ASCII = KeyboardMode(2u)
val FullLatin = KeyboardMode(3u)
val Alphabet = KeyboardMode(4u)
val SimplifiedChinese = KeyboardMode(5u)
val TraditionalChinese = KeyboardMode(6u)
val Korean = KeyboardMode(7u)
val LanguageSet2 = KeyboardMode(8u)
val LanguageSet2Latin = KeyboardMode(9u)
}
}
/**
* This data class matches InvalidCharFlags in skyline/applet/swkbd/software_keyboard_config.h
*/
data class InvalidCharFlags(
var flags : u32 = 0u
) : ByteBufferSerializable {
// Access each flag as a bool
var space : Boolean
get() : Boolean = (flags and 0b1u shl 1) != 0u
set(value) {
flags = flags and (0b1u shl 1).inv() or (if (value) 0b1u shl 1 else 0u)
}
var atMark : Boolean
get() : Boolean = (flags and 0b1u shl 2) != 0u
set(value) {
flags = flags and (0b1u shl 2).inv() or (if (value) 0b1u shl 2 else 0u)
}
var percent : Boolean
get() : Boolean = (flags and 0b1u shl 3) != 0u
set(value) {
flags = flags and (0b1u shl 3).inv() or (if (value) 0b1u shl 3 else 0u)
}
var slash : Boolean
get() : Boolean = (flags and 0b1u shl 4) != 0u
set(value) {
flags = flags and (0b1u shl 4).inv() or (if (value) 0b1u shl 4 else 0u)
}
var backSlash : Boolean
get() : Boolean = (flags and 0b1u shl 5) != 0u
set(value) {
flags = flags and (0b1u shl 5).inv() or (if (value) 0b1u shl 5 else 0u)
}
var numeric : Boolean
get() : Boolean = (flags and 0b1u shl 6) != 0u
set(value) {
flags = flags and (0b1u shl 6).inv() or (if (value) 0b1u shl 6 else 0u)
}
var outsideOfDownloadCode : Boolean
get() : Boolean = (flags and 0b1u shl 7) != 0u
set(value) {
flags = flags and (0b1u shl 7).inv() or (if (value) 0b1u shl 7 else 0u)
}
var outsideOfMiiNickName : Boolean
get() : Boolean = (flags and 0b1u shl 8) != 0u
set(value) {
flags = flags and (0b1u shl 8).inv() or (if (value) 0b1u shl 8 else 0u)
}
}
/**
* This data class matches InitialCursorPos in skyline/applet/swkbd/software_keyboard_config.h
*/
data class InitialCursorPos(
var pos : u32 = 0u
) : ByteBufferSerializable {
companion object {
val First = InitialCursorPos(0u)
val Last = InitialCursorPos(1u)
}
}
/**
* This data class matches PasswordMode in skyline/applet/swkbd/software_keyboard_config.h
*/
data class PasswordMode(
var mode : u32 = 0u
) : ByteBufferSerializable {
companion object {
val Show = PasswordMode(0u)
val Hide = PasswordMode(1u)
}
}
/**
* This data class matches InputFormMode in skyline/applet/swkbd/software_keyboard_config.h
*/
data class InputFormMode(
var mode : u32 = 0u
) : ByteBufferSerializable {
companion object {
val OneLine = InputFormMode(0u)
val MultiLine = InputFormMode(1u)
val Separate = InputFormMode(2u)
}
}
/**
* This data class matches DictionaryLanguage in skyline/applet/swkbd/software_keyboard_config.h
*/
data class DictionaryLanguage(
var lang : u16 = 0u
) : ByteBufferSerializable
/**
* This data class matches DictionaryInfo in skyline/applet/swkbd/software_keyboard_config.h
*/
data class DictionaryInfo(
var offset : u32 = 0u,
var size : u16 = 0u,
var dictionaryLang : DictionaryLanguage = DictionaryLanguage()
) : ByteBufferSerializable
/**
* This data class matches KeyboardConfigVB in skyline/applet/swkbd/software_keyboard_config.h
*/
data class SoftwareKeyboardConfig(
var keyboardMode : KeyboardMode = KeyboardMode(),
@param:ByteBufferSerializable.ByteBufferSerializableArray(0x9) val okText : CharArray = CharArray(0x9),
var leftOptionalSymbolKey : Char = '\u0000',
var rightOptionalSymbolKey : Char = '\u0000',
var isPredictionEnabled : bool = false,
@param:ByteBufferSerializable.ByteBufferSerializablePadding(0x1) val _pad0_ : ByteBufferSerializable.ByteBufferPadding = ByteBufferSerializable.ByteBufferPadding,
var invalidCharsFlags : InvalidCharFlags = InvalidCharFlags(),
var initialCursorPos : InitialCursorPos = InitialCursorPos(),
@param:ByteBufferSerializable.ByteBufferSerializableArray(0x41) val headerText : CharArray = CharArray(0x41),
@param:ByteBufferSerializable.ByteBufferSerializableArray(0x81) val subText : CharArray = CharArray(0x81),
@param:ByteBufferSerializable.ByteBufferSerializableArray(0x101) val guideText : CharArray = CharArray(0x101),
@param:ByteBufferSerializable.ByteBufferSerializablePadding(0x2) val _pad2_ : ByteBufferSerializable.ByteBufferPadding = ByteBufferSerializable.ByteBufferPadding,
var textMaxLength : u32 = 0u,
var textMinLength : u32 = 0u,
var passwordMode : PasswordMode = PasswordMode(),
var inputFormMode : InputFormMode = InputFormMode(),
var isUseNewLine : bool = false,
var isUseUtf8 : bool = false,
var isUseBlurBackground : bool = false,
@param:ByteBufferSerializable.ByteBufferSerializablePadding(0x1) val _pad3_ : ByteBufferSerializable.ByteBufferPadding = ByteBufferSerializable.ByteBufferPadding,
var initialStringOffset : u32 = 0u,
var initialStringLength : u32 = 0u,
var userDictionaryOffset : u32 = 0u,
var userDictionaryNum : u32 = 0u,
var isUseTextCheck : bool = false,
@param:ByteBufferSerializable.ByteBufferSerializablePadding(0x3) val reserved0 : ByteBufferSerializable.ByteBufferPadding = ByteBufferSerializable.ByteBufferPadding,
@param:ByteBufferSerializable.ByteBufferSerializableArray(0x8) val separateTextPos : u32Array = u32Array(0x8),
@param:ByteBufferSerializable.ByteBufferSerializableArray(0x18) val customizedDicInfoList : Array<DictionaryInfo> = Array(0x18) { DictionaryInfo() },
var customizedDicCount : u8 = 0u,
var isCancelButtonDisabled : bool = false,
@param:ByteBufferSerializable.ByteBufferSerializablePadding(0xD) val reserved1 : ByteBufferSerializable.ByteBufferPadding = ByteBufferSerializable.ByteBufferPadding,
var trigger : u8 = 0u,
@param:ByteBufferSerializable.ByteBufferSerializablePadding(0x4) val reserved2 : ByteBufferSerializable.ByteBufferPadding = ByteBufferSerializable.ByteBufferPadding,
) : ByteBufferSerializable {
fun isValid(codepoint : Int) : Boolean {
when (keyboardMode) {
KeyboardMode.Numeric -> {
if (!(codepoint in '0'.code..'9'.code || codepoint == leftOptionalSymbolKey.code || codepoint == rightOptionalSymbolKey.code))
return false
}
KeyboardMode.ASCII -> {
if (codepoint !in 0x00..0x7F)
return false
}
}
if (invalidCharsFlags.space && Character.isSpaceChar(codepoint))
return false
if (invalidCharsFlags.atMark && codepoint == '@'.code)
return false
if (invalidCharsFlags.slash && codepoint == '/'.code)
return false
if (invalidCharsFlags.backSlash && codepoint == '\\'.code)
return false
if (invalidCharsFlags.numeric && codepoint in '0'.code..'9'.code)
return false
if (invalidCharsFlags.outsideOfDownloadCode && codepoint !in DownloadCodeCodepoints)
return false
if (invalidCharsFlags.outsideOfMiiNickName && codepoint in OutsideOfMiiNicknameCodepoints)
return false
if (!isUseNewLine && codepoint == '\n'.code)
return false
return true
}
fun isValid(string : String) : Boolean {
if (string.length !in textMinLength.toInt()..textMaxLength.toInt())
return false
for (codepoint in string.codePoints()) {
if (!isValid(codepoint))
return false
}
return true
}
fun isValid(chars : CharSequence) : Boolean {
if (chars.length !in textMinLength.toInt()..textMaxLength.toInt())
return false
for (codepoint in chars.codePoints()) {
if (!isValid(codepoint))
return false
}
return true
}
}

View File

@ -0,0 +1,125 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.applet.swkbd
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.DialogFragment
import emu.skyline.databinding.KeyboardDialogBinding
import emu.skyline.utils.stringFromChars
import java.util.concurrent.FutureTask
data class SoftwareKeyboardResult(val cancelled : Boolean, val text : String)
class SoftwareKeyboardDialog : DialogFragment() {
private val config by lazy { requireArguments().getParcelable<SoftwareKeyboardConfig>("config")!! }
private val initialText by lazy { requireArguments().getString("initialText")!! }
private var stopped = false
companion object {
/**
* @param config Holds the [SoftwareKeyboardConfig] that will be used to create the keyboard between instances of this dialog
* @param initialText Holds the text that was set by the guest when the dialog was created
*/
fun newInstance(config : SoftwareKeyboardConfig, initialText : String) : SoftwareKeyboardDialog {
val args = Bundle()
args.putParcelable("config", config)
args.putString("initialText", initialText)
val fragment = SoftwareKeyboardDialog()
fragment.arguments = args
return fragment
}
const val validationConfirm = 2
const val validationError = 1
}
private lateinit var binding : KeyboardDialogBinding
private var cancelled : Boolean = false
private var futureResult : FutureTask<SoftwareKeyboardResult> = FutureTask<SoftwareKeyboardResult> { return@FutureTask SoftwareKeyboardResult(cancelled, binding.textInput.text.toString()) }
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) = if (savedInstanceState?.getBoolean("stopped") != true) KeyboardDialogBinding.inflate(inflater).also { binding = it }.root else null
@SuppressLint("SetTextI18n")
override fun onViewCreated(view : View, savedInstanceState : Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (config.inputFormMode == InputFormMode.OneLine) {
binding.header.text = stringFromChars(config.headerText)
binding.header.visibility = View.VISIBLE
binding.sub.text = stringFromChars(config.subText)
binding.sub.visibility = View.VISIBLE
}
if (config.keyboardMode == KeyboardMode.Numeric) {
binding.textInput.inputType = if (config.passwordMode == PasswordMode.Hide) InputType.TYPE_NUMBER_VARIATION_PASSWORD else InputType.TYPE_CLASS_NUMBER
} else {
binding.textInput.inputType = if (config.passwordMode == PasswordMode.Hide) InputType.TYPE_TEXT_VARIATION_PASSWORD else InputType.TYPE_CLASS_TEXT
if (config.invalidCharsFlags.outsideOfDownloadCode)
binding.textInput.inputType = binding.textInput.inputType or InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
if (config.isUseNewLine)
binding.textInput.inputType = binding.textInput.inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
else
binding.textInput.inputType = binding.textInput.inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE.inv()
}
val okText = stringFromChars(config.okText)
if (okText.isNotBlank())
binding.okButton.text = okText
val guideText = stringFromChars(config.guideText)
if (guideText.isNotBlank())
binding.inputLayout.hint = guideText
binding.textInput.setText(initialText)
binding.textInput.setSelection(if (config.initialCursorPos == InitialCursorPos.First) 0 else initialText.length)
binding.textInput.filters = arrayOf(SoftwareKeyboardFilter(config))
binding.textInput.doOnTextChanged { text, _, _, _ ->
binding.okButton.isEnabled = config.isValid(text!!)
binding.lengthStatus.text = "${text.length}/${config.textMaxLength}"
}
binding.lengthStatus.text = "${initialText.length}/${config.textMaxLength}"
binding.okButton.isEnabled = config.isValid(initialText)
binding.okButton.setOnClickListener {
cancelled = false
futureResult.run()
}
if (config.isCancelButtonDisabled) {
binding.cancelButton.visibility = ViewGroup.GONE
} else {
binding.cancelButton.setOnClickListener {
cancelled = true
futureResult.run()
}
}
}
override fun onStart() {
stopped = false
super.onStart()
}
override fun onStop() {
stopped = true
super.onStop()
}
override fun onSaveInstanceState(outState : Bundle) {
outState.putBoolean("stopped", stopped)
super.onSaveInstanceState(outState)
}
fun waitForSubmitOrCancel() : SoftwareKeyboardResult {
val result = futureResult.get()
futureResult = FutureTask<SoftwareKeyboardResult> { return@FutureTask SoftwareKeyboardResult(cancelled, binding.textInput.text.toString()) }
return result
}
}

View File

@ -0,0 +1,66 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.applet.swkbd
import android.text.InputFilter
import android.text.SpannableStringBuilder
import android.text.Spanned
class SoftwareKeyboardFilter(val config : SoftwareKeyboardConfig) : InputFilter {
override fun filter(source : CharSequence, start : Int, end : Int, dest : Spanned, dstart : Int, dend : Int) : CharSequence? {
val filteredStringBuilder = SpannableStringBuilder()
val newCharacterIndex = arrayOfNulls<Int>(end - start)
var currentIndex : Int
val sourceCodepoints = source.subSequence(start, end).codePoints().iterator()
val replacedLength = dest.subSequence(dstart, dend).length
var nextIndex = start
while (sourceCodepoints.hasNext()) {
var codepoint = sourceCodepoints.next()
currentIndex = nextIndex
nextIndex += Character.charCount(codepoint)
if (config.invalidCharsFlags.outsideOfDownloadCode)
codepoint = Character.toUpperCase(codepoint)
if (!config.isValid(codepoint))
continue
// Check if the string would be over the maximum length after adding this character
if (dest.length - replacedLength + filteredStringBuilder.length + Character.charCount(codepoint) > config.textMaxLength.toInt())
break
else
// Keep track where in the new string each character ended up
newCharacterIndex[currentIndex - start] = filteredStringBuilder.length
filteredStringBuilder.append(String(Character.toChars(codepoint)))
}
if (source is Spanned) {
// Copy the spans from the source to the returned SpannableStringBuilder
for (span in source.getSpans(start, end, Any::class.java)) {
val spanStart = source.getSpanStart(span) - start
val spanEnd = source.getSpanEnd(span) - start
currentIndex = spanStart
var newStart = newCharacterIndex[currentIndex]
// Make the new span's start be at the first character that was in the span's range and wasn't removed
while (newStart == null && currentIndex < spanEnd - 1) {
newStart = newCharacterIndex[++currentIndex]
}
currentIndex = spanEnd - 1
var newEnd = newCharacterIndex[currentIndex]
// Make the span end be at the last character that was in the span's range and wasn't removed
while (newEnd == null && currentIndex > spanStart) {
newEnd = newCharacterIndex[--currentIndex]
}
if(newStart != null && newEnd != null)
filteredStringBuilder.setSpan(span, newStart, newEnd + 1, source.getSpanFlags(span))
}
}
return filteredStringBuilder
}
}

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/input_dialog"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundColor"
app:layout_anchorGravity="center_vertical">
<LinearLayout
android:id="@+id/horizontal_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.05">
<Space
android:id="@+id/space_left"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="0.05" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.90">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimary"
android:textSize="24sp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/sub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"
android:textSize="20sp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/header" />
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_button"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/grid_padding"
android:layout_marginEnd="@dimen/grid_padding"
android:text="@android:string/cancel"
app:layout_constraintEnd_toStartOf="@+id/ok_button"
app:layout_constraintTop_toBottomOf="@+id/length_status" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ok_button"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/grid_padding"
android:text="@android:string/ok"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/length_status" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/grid_padding"
android:hint="@string/input_hint"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sub">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="flagNoExtractUi|flagNoFullscreen"
android:inputType="text"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/length_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/grid_padding"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/input_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>
<Space
android:id="@+id/space_right"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="0.05" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -166,6 +166,8 @@
<string name="open_sans_description">Open Sans is used as our FOSS shared font replacement for Latin, Korean and Chinese</string>
<string name="roboto_description">Roboto is used as our FOSS shared font replacement for Nintendo\'s extended character set</string>
<string name="source_sans_pro_description">Source Sans Pro is used as our FOSS shared font replacement for Nintendo\'s extended Chinese character set</string>
<!-- Software Keyboard -->
<string name="input_hint">Input Text</string>
<!-- Misc -->
<!--suppress AndroidLintUnusedResources -->
<string name="expand_button_title" tools:override="true">Expand</string>